diff --git a/app/MindWork AI Studio/Models/DeepSeek/DeepSeekFamily.cs b/app/MindWork AI Studio/Models/DeepSeek/DeepSeekFamily.cs new file mode 100644 index 00000000..1cfb487c --- /dev/null +++ b/app/MindWork AI Studio/Models/DeepSeek/DeepSeekFamily.cs @@ -0,0 +1,77 @@ +using static AIStudio.Provider.Capability; + +namespace AIStudio.Models.DeepSeek; + +/// +/// DeepSeek, from V3 to V4, including R1 and the checkpoints distilled from it. +/// +/// +/// One family for all of it, and bound to no provider: DeepSeek publishes its models as open +/// weights and offers them on its own platform under the very same names, so a rule written once +/// answers wherever the model turns up. The old code arrived at that by having its DeepSeek +/// function call the open weights function, which is one of the loops this rebuild is undoing. +/// +/// The distills are the case the whole priority question came from. They are Llama and Qwen +/// checkpoints fine-tuned on R1 answers, so they carry "r1" in their name and would be read as R1 +/// itself -- which would promise the tool calling they lost together with R1's chat template. Here +/// the rule for them is the R1 rule with one condition more, and that alone decides it. +/// +/// Point releases behind a dot need a line of their own, as everywhere: "deepseek-v4" does not +/// answer for "deepseek-v4.1", because a dot separates versions rather than name parts. +/// +public sealed class DeepSeekFamily : ModelFamily +{ + /// + public override ModelVendor Vendor => ModelVendor.DEEP_SEEK; + + /// + public override ModelSource Source => new("https://api-docs.deepseek.com/", new DateOnly(2026, 9, 11), "Ported unchanged from the rules in ProviderExtensions.DeepSeek.cs and the DeepSeek block of ProviderExtensions.OpenSource.cs."); + + /// + protected override void Declare(ModelFamilyBuilder builder) + { + builder.Rule("deepseek").AsSegment() + .Capabilities(TEXT_INPUT | TEXT_OUTPUT) + .Apis(CHAT_COMPLETION_API); + + // The V3 line answers directly and calls functions: + builder.Rule("deepseek-v3").AsPrefix().Inherits() + .Capabilities(FUNCTION_CALLING); + + // + // From V3.1 on there is a thinking mode which the request turns on, and V3.2 added tool + // calling inside it. The gateways write these either as "deepseek-v3.1" or as + // "deepseek-chat-v3.1", so the version alone is what is looked for. + // + builder.Rule("deepseek").AsSegment().AlsoContains("v3.1").InheritsFrom("deepseek-v3") + .Reasoning(ReasoningSupport.OPTIONAL); + + builder.Rule("deepseek").AsSegment().AlsoContains("v3.2").InheritsFrom("deepseek-v3") + .Reasoning(ReasoningSupport.OPTIONAL); + + builder.Rule("deepseek-r1").AsPrefix().InheritsFrom("deepseek-v3") + .Reasoning(ReasoningSupport.ALWAYS); + + // The distills kept the chat template of the model they were built from, so none of the + // tool calling R1 itself was trained for survived: + builder.Rule("deepseek-r1").AsPrefix().AlsoContains("distill").Inherits() + .Removes(FUNCTION_CALLING); + + builder.Rule("deepseek-v4").AsPrefix().InheritsFrom("deepseek-v3") + .Reasoning(ReasoningSupport.ON_BY_DEFAULT); + + builder.Rule("deepseek-v4").AsPrefix().AlsoContains("vision").Inherits() + .Capabilities(MULTIPLE_IMAGE_INPUT); + + // + // The two aliases of DeepSeek's own platform. They name a mode rather than a model: both + // point at the current flash model, one with thinking and one without. Exactly these + // names and no others -- "deepseek-chat-v3.1" is a gateway's name for a version, not this + // alias. + // + builder.Rule("deepseek-chat").AsExact().InheritsFrom("deepseek-v3"); + + builder.Rule("deepseek-reasoner").AsExact().InheritsFrom("deepseek-v3") + .Reasoning(ReasoningSupport.ALWAYS); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Models/ModelFamilyBuilder.cs b/app/MindWork AI Studio/Models/ModelFamilyBuilder.cs index 559c99cf..11b8f80e 100644 --- a/app/MindWork AI Studio/Models/ModelFamilyBuilder.cs +++ b/app/MindWork AI Studio/Models/ModelFamilyBuilder.cs @@ -36,9 +36,20 @@ public sealed class ModelFamilyBuilder(string origin) /// The rules, in the order they were stated. internal IReadOnlyList Build() { + // + // The same text may well be stated twice, with different conditions on top -- that is how + // a variant of a generation is written. What cannot be done is naming that text to inherit + // from, because it names two rules and taking either of them would be a coin toss. Found + // before anything is built, so that where the two stand in the file makes no difference. + // + var statedMoreThanOnce = this.stated + .GroupBy(statement => statement.PatternText, StringComparer.Ordinal) + .Where(group => group.Count() > 1) + .Select(group => group.Key) + .ToHashSet(StringComparer.Ordinal); + var built = new List(this.stated.Count); var byPatternText = new Dictionary(StringComparer.Ordinal); - var statedMoreThanOnce = new HashSet(StringComparer.Ordinal); ModelProfileChange? previous = null; foreach (var statement in this.stated) @@ -46,15 +57,7 @@ public sealed class ModelFamilyBuilder(string origin) var rule = statement.Build(statement.InheritanceBasis(byPatternText, statedMoreThanOnce, previous)); built.Add(rule); - - // - // The same text may well be stated twice, with different conditions on top -- that is - // how a variant of a generation is written. What cannot be done afterwards is naming - // that text to inherit from, because it no longer names one rule. - // - if (!byPatternText.TryAdd(rule.Pattern.Text, rule.Change)) - statedMoreThanOnce.Add(rule.Pattern.Text); - + byPatternText[rule.Pattern.Text] = rule.Change; previous = rule.Change; } diff --git a/app/MindWork AI Studio/Models/ModelRuleBuilder.cs b/app/MindWork AI Studio/Models/ModelRuleBuilder.cs index 9e2a0753..6f4376ef 100644 --- a/app/MindWork AI Studio/Models/ModelRuleBuilder.cs +++ b/app/MindWork AI Studio/Models/ModelRuleBuilder.cs @@ -34,6 +34,15 @@ public sealed class ModelRuleBuilder(string patternText, ModelRuleKind ruleKind, 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. /// diff --git a/app/MindWork AI Studio/Models/Perplexity/SonarFamily.cs b/app/MindWork AI Studio/Models/Perplexity/SonarFamily.cs new file mode 100644 index 00000000..6a26fefe --- /dev/null +++ b/app/MindWork AI Studio/Models/Perplexity/SonarFamily.cs @@ -0,0 +1,36 @@ +using static AIStudio.Provider.Capability; + +namespace AIStudio.Models.Perplexity; + +/// +/// Sonar, the Perplexity models which search the web before they answer. +/// +/// +/// Searching is what they are, not something they can be asked to do, so every one of them states +/// it. What differs is only whether the model thinks as well. +/// +/// No Sonar writes images. What looks like it does is the option to have the answer come with +/// pictures: those are images the search found on the pages it read, handed back as links, and +/// reporting that as an output modality would have the chat wait for pictures which never arrive. +/// +public sealed class SonarFamily : ModelFamily +{ + /// + public override ModelVendor Vendor => ModelVendor.PERPLEXITY; + + /// + public override ModelSource Source => new("https://docs.perplexity.ai/getting-started/models", new DateOnly(2026, 9, 11), "Ported unchanged from the rules in ProviderExtensions.Perplexity.cs: images in, web search always, thinking for the reasoning and research models."); + + /// + protected override void Declare(ModelFamilyBuilder builder) + { + builder.Rule("sonar").AsSegment() + .Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT | WEB_SEARCH) + .Apis(CHAT_COMPLETION_API); + + builder.Rule("sonar").AsSegment().AlsoContains("reasoning").Inherits() + .Reasoning(ReasoningSupport.ALWAYS); + + builder.Rule("sonar").AsSegment().AlsoContains("deep-research").Inherits(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Models/XAI/GrokFamily.cs b/app/MindWork AI Studio/Models/XAI/GrokFamily.cs new file mode 100644 index 00000000..6a1fb4f7 --- /dev/null +++ b/app/MindWork AI Studio/Models/XAI/GrokFamily.cs @@ -0,0 +1,58 @@ +using static AIStudio.Provider.Capability; + +namespace AIStudio.Models.XAI; + +/// +/// Grok, from the old vision models to the 5 line. +/// +/// +/// The family's own fallback calls functions, and that is deliberate: without it an unknown Grok +/// version would reach whatever answers for everything and lose tool calling, which every Grok +/// since the 3 line has. Grok 3 itself needs no rule for the same reason -- the fallback already +/// says exactly what it is. +/// +/// Video is not among their modalities. xAI serves audio, image, and video through models and APIs +/// of their own, and the model pages of the 4.x line say "text, image" and nothing else. +/// +public sealed class GrokFamily : ModelFamily +{ + /// + public override ModelVendor Vendor => ModelVendor.XAI; + + /// + public override ModelSource Source => new("https://docs.x.ai/docs/models", new DateOnly(2026, 9, 11), "Ported unchanged from the Grok block of ProviderExtensions.OpenSource.cs."); + + /// + protected override void Declare(ModelFamilyBuilder builder) + { + builder.Rule("grok").AsSegment() + .Capabilities(TEXT_INPUT | TEXT_OUTPUT | FUNCTION_CALLING) + .Apis(CHAT_COMPLETION_API); + + // The old vision models look at pictures and call nothing: + builder.Rule("grok").AsSegment().AlsoContains("vision") + .Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT) + .Apis(CHAT_COMPLETION_API); + + builder.Rule("grok-3-mini").AsPrefix() + .Capabilities(TEXT_INPUT | TEXT_OUTPUT | FUNCTION_CALLING) + .Apis(CHAT_COMPLETION_API) + .Reasoning(ReasoningSupport.ALWAYS); + + // + // The 4 line reads images and always thinks; only the effort can be set. The 4.20 models + // need a line of their own because a dot separates versions rather than name parts, so + // "grok-4" does not answer for "grok-4.20". + // + builder.Rule("grok-4").AsPrefix() + .Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT | FUNCTION_CALLING) + .Apis(CHAT_COMPLETION_API) + .Reasoning(ReasoningSupport.ALWAYS); + + builder.Rule("grok-4.20").AsPrefix().InheritsFrom("grok-4"); + + // One member of the 4.20 line answers without thinking, and it says so in its name: + builder.Rule("grok-4.20").AsPrefix().AlsoContains("non-reasoning").Inherits() + .Reasoning(ReasoningSupport.NONE); + } +} \ No newline at end of file diff --git a/app/Tests/Models/PortingDifferenceTests.cs b/app/Tests/Models/PortingDifferenceTests.cs index 62097932..cf3b5fe4 100644 --- a/app/Tests/Models/PortingDifferenceTests.cs +++ b/app/Tests/Models/PortingDifferenceTests.cs @@ -34,6 +34,9 @@ public sealed class PortingDifferenceTests LLMProviders.GOOGLE, LLMProviders.MISTRAL, LLMProviders.ALIBABA_CLOUD, + LLMProviders.DEEP_SEEK, + LLMProviders.PERPLEXITY, + LLMProviders.X, ]; [Test]