mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 00:53:37 +00:00
Port the DeepSeek, Perplexity, and xAI families
This commit is contained in:
parent
5905f074b6
commit
402da2da96
77
app/MindWork AI Studio/Models/DeepSeek/DeepSeekFamily.cs
Normal file
77
app/MindWork AI Studio/Models/DeepSeek/DeepSeekFamily.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using static AIStudio.Provider.Capability;
|
||||
|
||||
namespace AIStudio.Models.DeepSeek;
|
||||
|
||||
/// <summary>
|
||||
/// DeepSeek, from V3 to V4, including R1 and the checkpoints distilled from it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public sealed class DeepSeekFamily : ModelFamily
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override ModelVendor Vendor => ModelVendor.DEEP_SEEK;
|
||||
|
||||
/// <inheritdoc />
|
||||
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.");
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -36,9 +36,20 @@ public sealed class ModelFamilyBuilder(string origin)
|
||||
/// <returns>The rules, in the order they were stated.</returns>
|
||||
internal IReadOnlyList<ModelRule> 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<ModelRule>(this.stated.Count);
|
||||
var byPatternText = new Dictionary<string, ModelProfileChange>(StringComparer.Ordinal);
|
||||
var statedMoreThanOnce = new HashSet<string>(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;
|
||||
}
|
||||
|
||||
|
||||
@ -34,6 +34,15 @@ public sealed class ModelRuleBuilder(string patternText, ModelRuleKind ruleKind,
|
||||
private TokenizerRef? tokenizer;
|
||||
private ImageLimits? images;
|
||||
|
||||
/// <summary>
|
||||
/// The text this rule answers for, before anything was stated about it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Read by the family builder before it builds anything, to find the texts which name more
|
||||
/// than one rule.
|
||||
/// </remarks>
|
||||
internal string PatternText => patternText;
|
||||
|
||||
/// <summary>
|
||||
/// The text is the whole model name.
|
||||
/// </summary>
|
||||
|
||||
36
app/MindWork AI Studio/Models/Perplexity/SonarFamily.cs
Normal file
36
app/MindWork AI Studio/Models/Perplexity/SonarFamily.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using static AIStudio.Provider.Capability;
|
||||
|
||||
namespace AIStudio.Models.Perplexity;
|
||||
|
||||
/// <summary>
|
||||
/// Sonar, the Perplexity models which search the web before they answer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public sealed class SonarFamily : ModelFamily
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override ModelVendor Vendor => ModelVendor.PERPLEXITY;
|
||||
|
||||
/// <inheritdoc />
|
||||
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.");
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
}
|
||||
}
|
||||
58
app/MindWork AI Studio/Models/XAI/GrokFamily.cs
Normal file
58
app/MindWork AI Studio/Models/XAI/GrokFamily.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using static AIStudio.Provider.Capability;
|
||||
|
||||
namespace AIStudio.Models.XAI;
|
||||
|
||||
/// <summary>
|
||||
/// Grok, from the old vision models to the 5 line.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public sealed class GrokFamily : ModelFamily
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override ModelVendor Vendor => ModelVendor.XAI;
|
||||
|
||||
/// <inheritdoc />
|
||||
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.");
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -34,6 +34,9 @@ public sealed class PortingDifferenceTests
|
||||
LLMProviders.GOOGLE,
|
||||
LLMProviders.MISTRAL,
|
||||
LLMProviders.ALIBABA_CLOUD,
|
||||
LLMProviders.DEEP_SEEK,
|
||||
LLMProviders.PERPLEXITY,
|
||||
LLMProviders.X,
|
||||
];
|
||||
|
||||
[Test]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user