diff --git a/app/MindWork AI Studio/Models/Microsoft/E5Family.cs b/app/MindWork AI Studio/Models/Microsoft/E5Family.cs new file mode 100644 index 00000000..341acd3f --- /dev/null +++ b/app/MindWork AI Studio/Models/Microsoft/E5Family.cs @@ -0,0 +1,31 @@ +using AIStudio.Provider; + +using static AIStudio.Provider.Capability; + +namespace AIStudio.Models.Microsoft; + +/// +/// E5, the embedding models built on somebody else's weights. +/// +/// +/// "e5-mistral-7b-instruct" is what made this a family of its own. It is an embedding model, and it +/// carries the name of the model it was trained from, so the Mistral rules answer for it and tell +/// it that it chats and calls functions. Saying which name means what it says is cheaper than +/// teaching every family whose weights somebody built an embedder from. +/// +/// The E5 part is the whole statement: the rest of the name says nothing about what the model does. +/// +public sealed class E5Family : ModelFamily +{ + /// + public override ModelVendor Vendor => ModelVendor.MICROSOFT; + + /// + public override ModelSource Source => new("https://huggingface.co/intfloat/e5-mistral-7b-instruct", new DateOnly(2026, 9, 11), "The app lists this under IProvider.GetEmbeddingModels, which is where the statement that it embeds comes from."); + + /// + protected override void Declare(ModelFamilyBuilder builder) => + builder.Rule("e5").AsSegment() + .Capabilities(TEXT_INPUT | EMBEDDING) + .Kind(ModelKind.EMBEDDING); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Models/Mistral/MistralFamily.cs b/app/MindWork AI Studio/Models/Mistral/MistralFamily.cs new file mode 100644 index 00000000..9c1246c8 --- /dev/null +++ b/app/MindWork AI Studio/Models/Mistral/MistralFamily.cs @@ -0,0 +1,37 @@ +using static AIStudio.Provider.Capability; + +namespace AIStudio.Models.Mistral; + +/// +/// The Mistral models which carry no further family name: Mistral 7B, Mistral 3, and their kin. +/// +/// +/// The open weights are where these live. Mistral's own API sells the named ranges -- Small, +/// Medium, Large -- while the plain checkpoints are the ones people run themselves, which is why +/// nothing here is dated: those names carry a size and a quantization instead of a release. +/// +/// A substring, and it has to be one: this is the fallback of the whole range, and every family +/// with a name of its own beats it by saying more. What it must not do is claim Ministral or +/// Magistral, and it does not -- neither of those two names contains "mistral". +/// +public sealed class MistralFamily : ModelFamily +{ + /// + public override ModelVendor Vendor => ModelVendor.MISTRAL_AI; + + /// + public override ModelSource Source => new("https://docs.mistral.ai/getting-started/models/weights/", new DateOnly(2026, 9, 11), "Ported unchanged from the Mistral block of ProviderExtensions.OpenSource.cs: its default answer, and the rule for the 3 line."); + + /// + protected override void Declare(ModelFamilyBuilder builder) + { + builder.Rule("mistral").AsSubstring() + .Capabilities(TEXT_INPUT | TEXT_OUTPUT | FUNCTION_CALLING) + .Apis(CHAT_COMPLETION_API); + + // The 3 line reads images and thinks when it is asked to: + builder.Rule("mistral-3").AsSegment().Inherits() + .Capabilities(MULTIPLE_IMAGE_INPUT) + .Reasoning(ReasoningSupport.OPTIONAL); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Models/Mistral/MistralReleases.cs b/app/MindWork AI Studio/Models/Mistral/MistralReleases.cs index 2d15e220..d18d8f84 100644 --- a/app/MindWork AI Studio/Models/Mistral/MistralReleases.cs +++ b/app/MindWork AI Studio/Models/Mistral/MistralReleases.cs @@ -49,7 +49,9 @@ public static class MistralReleases /// /// Mistral serves some models under their marketing version as well, and writes the version /// separator both ways: mistral-medium-3.5 and mistral-medium-3-5 are the same model. Those - /// names carry no release date, so they are mapped onto the release they stand for. + /// names carry no release date, so they are mapped onto the release they stand for. Ollama + /// leaves the separator out altogether for the Small checkpoints, which is a third spelling of + /// the same statement. /// /// The order matters, and it is the one place in this rebuild where it still does: these are /// read as plain text rather than as patterns, so "mistral-medium-3" would answer for @@ -71,6 +73,11 @@ public static class MistralReleases ("mistral-small-3.1", 2503), ("mistral-small-3-1", 2503), ("mistral-small-3", 2501), + + ("mistral-small4", 2603), + ("mistral-small3.2", 2506), + ("mistral-small3.1", 2503), + ("mistral-small3", 2501), ]; /// diff --git a/app/MindWork AI Studio/Models/Mistral/MistralSmallFamily.cs b/app/MindWork AI Studio/Models/Mistral/MistralSmallFamily.cs index 886df374..a75b04b6 100644 --- a/app/MindWork AI Studio/Models/Mistral/MistralSmallFamily.cs +++ b/app/MindWork AI Studio/Models/Mistral/MistralSmallFamily.cs @@ -5,6 +5,12 @@ namespace AIStudio.Models.Mistral; /// /// Mistral Small, which gained images with 3.1 and reasoning with 4. /// +/// +/// The one family of the range whose name arrives glued to its version: Ollama publishes the open +/// weights as "mistral-small3.1" and "mistral-small3.2", without the separator Mistral's own API +/// writes. A substring covers both spellings, and it stays specific enough that nothing else in the +/// range can be mistaken for it. +/// public sealed class MistralSmallFamily : MistralReleaseDatedFamily { /// @@ -21,7 +27,7 @@ public sealed class MistralSmallFamily : MistralReleaseDatedFamily /// protected override void Declare(ModelFamilyBuilder builder) => - builder.Rule("mistral-small").AsSegment() + builder.Rule("mistral-small").AsSubstring() .Capabilities(WHAT_THEY_COULD_ALWAYS_DO) .Apis(CHAT_COMPLETION_API); } \ No newline at end of file diff --git a/app/MindWork AI Studio/Models/ZAI/GlmFamily.cs b/app/MindWork AI Studio/Models/ZAI/GlmFamily.cs new file mode 100644 index 00000000..d63499d8 --- /dev/null +++ b/app/MindWork AI Studio/Models/ZAI/GlmFamily.cs @@ -0,0 +1,82 @@ +using AIStudio.Models.Matching; + +using static AIStudio.Provider.Capability; + +namespace AIStudio.Models.ZAI; + +/// +/// GLM, from Z AI. +/// +/// +/// Two things about these names need saying. Z AI writes the version with a dot, but Mistral serves +/// the same models as "glm-5-2" and "zai-glm-5-2", so each generation is stated in both spellings. +/// And a vision model is marked by a "v" glued to the version number -- glm-4v, glm-4.1v, glm-4.5v +/// -- which is not a name part and therefore not something a pattern can ask about. That is what +/// the refinement below is for. +/// +/// Looking for a bare "v" anywhere, which the previous rules started out doing, calls every +/// quantized build a vision model: "nvfp4" carries one, and so does the name of more than one +/// inference provider. The digit in front is what makes it a version marker. +/// +public sealed class GlmFamily : ModelFamily +{ + /// + public override ModelVendor Vendor => ModelVendor.Z_AI; + + /// + public override ModelSource Source => new("https://huggingface.co/zai-org", new DateOnly(2026, 9, 11), "Ported unchanged from the Z AI block of ProviderExtensions.OpenSource.cs."); + + /// + public override ModelProfile Refine(in ModelId id, in ModelProfile selected) + { + if (!MarksAVisionModel(id.Normalized.AsSpan())) + return selected; + + return selected with { Capabilities = selected.Capabilities | MULTIPLE_IMAGE_INPUT }; + } + + /// + protected override void Declare(ModelFamilyBuilder builder) + { + // Every other GLM thinks when the request asks it to: + builder.Rule("glm").AsSubstring() + .Capabilities(TEXT_INPUT | TEXT_OUTPUT | FUNCTION_CALLING) + .Apis(CHAT_COMPLETION_API) + .Reasoning(ReasoningSupport.OPTIONAL); + + // The 4 line answers straight away: + builder.Rule("glm-4").AsSegment() + .Capabilities(TEXT_INPUT | TEXT_OUTPUT | FUNCTION_CALLING) + .Apis(CHAT_COMPLETION_API); + + // 5.2 thinks unless it is told not to: + builder.Rule("glm-5.2").AsSegment() + .Capabilities(TEXT_INPUT | TEXT_OUTPUT | FUNCTION_CALLING) + .Apis(CHAT_COMPLETION_API) + .Reasoning(ReasoningSupport.ON_BY_DEFAULT); + + builder.Rule("glm-5-2").AsSegment().Inherits(); + + // 5.3 thinks whatever it is told: only the effort can be lowered, not the thinking itself. + builder.Rule("glm-5.3").AsSegment() + .Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT | FUNCTION_CALLING) + .Apis(CHAT_COMPLETION_API) + .Reasoning(ReasoningSupport.ALWAYS); + + builder.Rule("glm-5-3").AsSegment().Inherits(); + } + + /// + /// Whether the version number of this name is followed by the vision marker. + /// + /// The normalized model name. + /// True, when a "v" sits directly behind a digit. + private static bool MarksAVisionModel(ReadOnlySpan modelName) + { + for (var index = 1; index < modelName.Length; index++) + if (modelName[index] is 'v' && char.IsAsciiDigit(modelName[index - 1])) + return true; + + return false; + } +} \ No newline at end of file diff --git a/app/Tests/Models/Corpus/CapabilitySnapshot.txt b/app/Tests/Models/Corpus/CapabilitySnapshot.txt index 8bcc65f2..a69d962d 100644 --- a/app/Tests/Models/Corpus/CapabilitySnapshot.txt +++ b/app/Tests/Models/Corpus/CapabilitySnapshot.txt @@ -229,7 +229,6 @@ SELF_HOSTED | minimax-m2:latest | ALWAYS_REASONING, CHAT_COMPLETION_API, FUNCTIO SELF_HOSTED | minimax-text-01 | CHAT_COMPLETION_API, FUNCTION_CALLING, TEXT_INPUT, TEXT_OUTPUT SELF_HOSTED | ministral-8b-instruct-2410 | CHAT_COMPLETION_API, FUNCTION_CALLING, TEXT_INPUT, TEXT_OUTPUT SELF_HOSTED | mistral-nemo:12b | CHAT_COMPLETION_API, FUNCTION_CALLING, TEXT_INPUT, TEXT_OUTPUT -SELF_HOSTED | mistral-small-3.1-24b-instruct | CHAT_COMPLETION_API, FUNCTION_CALLING, MULTIPLE_IMAGE_INPUT, OPTIONAL_REASONING, TEXT_INPUT, TEXT_OUTPUT SELF_HOSTED | mistral-small3.2:24b | CHAT_COMPLETION_API, FUNCTION_CALLING, MULTIPLE_IMAGE_INPUT, TEXT_INPUT, TEXT_OUTPUT SELF_HOSTED | muse-glimmer-30b | ALWAYS_REASONING, CHAT_COMPLETION_API, FUNCTION_CALLING, MULTIPLE_IMAGE_INPUT, TEXT_INPUT, TEXT_OUTPUT SELF_HOSTED | nemotron-3-49b | CHAT_COMPLETION_API, FUNCTION_CALLING, REASONING_BY_DEFAULT, TEXT_INPUT, TEXT_OUTPUT diff --git a/app/Tests/Models/Corpus/ExpectedChanges.cs b/app/Tests/Models/Corpus/ExpectedChanges.cs index 9fcccb86..7099916c 100644 --- a/app/Tests/Models/Corpus/ExpectedChanges.cs +++ b/app/Tests/Models/Corpus/ExpectedChanges.cs @@ -122,6 +122,12 @@ public static class ExpectedChanges Reason: "Claude 3.5 Sonnet loses its image input when it arrives under the Bedrock spelling: the vendor sits behind a dot rather than a slash, so neither the gateway detection nor the reseller check finds it.", Source: "The same model as \"anthropic/claude-sonnet-4-0\" and the other Claude entries of this corpus, which all report image input."), + new(SELF_HOSTED, "mistral-small-3.1-24b-instruct", + AnswerToday: [TEXT_INPUT, MULTIPLE_IMAGE_INPUT, TEXT_OUTPUT, OPTIONAL_REASONING, FUNCTION_CALLING, CHAT_COMPLETION_API], + AnswerWanted: [TEXT_INPUT, MULTIPLE_IMAGE_INPUT, TEXT_OUTPUT, FUNCTION_CALLING, CHAT_COMPLETION_API], + Reason: "Mistral Small 3.1 is told that it thinks, and the very same model is told the opposite when it arrives through Mistral's own API. The rules for the open weights answer for the whole 3 and 4 range in one line, and reasoning arrived with 4.", + Source: "The corpus entry \"mistral-small-2503\" is this model at Mistral and reports no reasoning; Mistral names Magistral as the thinking model of that generation."), + new(HELMHOLTZ, "01 - GPT-5.5 - great overall performance", AnswerToday: [TEXT_INPUT, MULTIPLE_IMAGE_INPUT, TEXT_OUTPUT, FUNCTION_CALLING, WEB_SEARCH, CHAT_COMPLETION_API], AnswerWanted: [TEXT_INPUT, MULTIPLE_IMAGE_INPUT, TEXT_OUTPUT, FUNCTION_CALLING, REASONING_BY_DEFAULT, WEB_SEARCH, CHAT_COMPLETION_API], diff --git a/app/Tests/Models/PortingDifferenceTests.cs b/app/Tests/Models/PortingDifferenceTests.cs index 57c0b1a4..efe2072d 100644 --- a/app/Tests/Models/PortingDifferenceTests.cs +++ b/app/Tests/Models/PortingDifferenceTests.cs @@ -56,21 +56,20 @@ public sealed class PortingDifferenceTests /// /// Only vendors, never UNKNOWN: that is what a model nobody wrote a rule for answers with, and /// putting it here would compare everything against everything. - /// - /// Mistral is the one ported vendor still missing. Its cloud writes a release date into every - /// name and its rules are built on that, while the open weights are called - /// "mistral-small-3.1-24b-instruct" and carry none -- so those names are still to be ported. /// private static readonly IReadOnlyList VENDORS_ALREADY_PORTED = [ ModelVendor.OPEN_AI, ModelVendor.ANTHROPIC, ModelVendor.GOOGLE, + ModelVendor.MISTRAL_AI, ModelVendor.ALIBABA, ModelVendor.DEEP_SEEK, ModelVendor.PERPLEXITY, ModelVendor.XAI, ModelVendor.META, + ModelVendor.Z_AI, + ModelVendor.MICROSOFT, ]; /// diff --git a/app/Tests/Models/ZAI/GlmFamilyTests.cs b/app/Tests/Models/ZAI/GlmFamilyTests.cs new file mode 100644 index 00000000..79c313f8 --- /dev/null +++ b/app/Tests/Models/ZAI/GlmFamilyTests.cs @@ -0,0 +1,41 @@ +using AIStudio.Models.Registry; +using AIStudio.Provider; + +namespace AIStudio.Tests.Models.ZAI; + +/// +/// Checks how a GLM name says that the model looks at pictures. +/// +/// +/// Z AI marks its vision models by gluing a "v" to the version number: glm-4v, glm-4.1v, glm-4.5v. +/// That is not a name part, so no pattern can ask about it and the family works it out of the name +/// instead -- the second of the two places in the rebuilt rules where a capability is calculated. +/// +/// These need tests of their own because the corpus cannot tell the calculation apart from a +/// careless one. Looking for a bare "v" anywhere answers every corpus name the same way, and is +/// still wrong: a quantized build carries one in "nvfp4", and so do the names of several inference +/// providers. The corpus happens to hold that name only for a generation which reads images anyway. +/// +[TestFixture] +public sealed class GlmFamilyTests +{ + [TestCase("glm-4.5v", TestName = "The vision marker sits behind the version")] + [TestCase("glm-4v", TestName = "A version without a dot carries the marker just the same")] + [TestCase("glm-4.1v-9b", TestName = "A size may follow the marker")] + public void AGlmWhoseVersionCarriesTheMarkerLooksAtPictures(string modelId) + { + var profile = ModelRegistry.Shared.Profile(LLMProviders.SELF_HOSTED, modelId); + + Assert.That(profile.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.True); + } + + [TestCase("glm-4-9b-chat-nvfp4", TestName = "A quantized build is not a vision model")] + [TestCase("glm-4-9b-chat", TestName = "The plain 4 line reads text only")] + [TestCase("glm-4.6-latest", TestName = "A rolling tag says nothing about pictures")] + public void AGlmCarryingAVSomewhereElseDoesNot(string modelId) + { + var profile = ModelRegistry.Shared.Profile(LLMProviders.SELF_HOSTED, modelId); + + Assert.That(profile.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.False); + } +} \ No newline at end of file