Port the Gemma, Nemotron, and Phi families

This commit is contained in:
Thorsten Sommer 2026-09-12 09:08:42 +02:00
parent ad3a25c80f
commit ac547ccb79
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
6 changed files with 192 additions and 1 deletions

View File

@ -0,0 +1,78 @@
using static AIStudio.Provider.Capability;
namespace AIStudio.Models.Google;
/// <summary>
/// Gemma, the open weights Google publishes next to Gemini.
/// </summary>
/// <remarks>
/// Two generations and one spelling problem. Ollama writes "gemma3:27b" and the hub writes
/// "gemma-3-27b-it", and no normalization turns one into the other, so each statement stands twice.
/// What it buys is that the rules never have to ask who served the model.
///
/// Tool calling is the line between the generations. What Google documents for Gemma 3 is writing
/// the tool descriptions into the prompt by hand, which is a different thing from what the tools
/// field of an OpenAI-compatible request does: the chat template has neither a tool role nor tool
/// tokens, and Ollama refuses a request carrying tools for these models. Gemma 4 is the first with
/// tokens of its own, and the first that thinks -- when the request opens the thinking channel.
/// </remarks>
public sealed class GemmaFamily : ModelFamily
{
/// <inheritdoc />
public override ModelVendor Vendor => ModelVendor.GOOGLE;
/// <inheritdoc />
public override ModelSource Source => new("https://ai.google.dev/gemma/docs/core", new DateOnly(2026, 9, 11), "Ported unchanged from the Gemma block of ProviderExtensions.OpenSource.cs.");
/// <inheritdoc />
protected override void Declare(ModelFamilyBuilder builder)
{
// The early generations take text only and were not built for tools:
builder.Rule("gemma").AsSubstring()
.Capabilities(TEXT_INPUT | TEXT_OUTPUT)
.Apis(CHAT_COMPLETION_API);
// Gemma 3 reads pictures from the 4B checkpoint upwards:
builder.Rule("gemma3").AsSubstring()
.Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT)
.Apis(CHAT_COMPLETION_API);
builder.Rule("gemma-3").AsSubstring().Inherits();
// The 1B checkpoint is the one that does not:
builder.Rule("gemma3").AsSubstring().AlsoContains("1b")
.Capabilities(TEXT_INPUT | TEXT_OUTPUT)
.Apis(CHAT_COMPLETION_API);
builder.Rule("gemma-3").AsSubstring().AlsoContains("1b").Inherits();
//
// The 3n checkpoints listen as well. Video is not a modality of any Gemma: the model cards
// list text, image, and audio, and mention video only as frames somebody else cut it into.
//
builder.Rule("gemma3n").AsSubstring()
.Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | AUDIO_INPUT | TEXT_OUTPUT)
.Apis(CHAT_COMPLETION_API);
builder.Rule("gemma-3n").AsSubstring().Inherits();
// Every checkpoint of Gemma 4 is multimodal; there is no text-only variant of it:
builder.Rule("gemma4").AsSubstring()
.Capabilities(TEXT_INPUT | MULTIPLE_IMAGE_INPUT | TEXT_OUTPUT | FUNCTION_CALLING)
.Apis(CHAT_COMPLETION_API)
.Reasoning(ReasoningSupport.OPTIONAL);
builder.Rule("gemma-4").AsSubstring().Inherits();
// Three of its checkpoints hear, and they are named one by one because that is all they
// have in common:
builder.Rule("gemma4").AsSubstring().AlsoContains("e2b").Inherits().Capabilities(AUDIO_INPUT);
builder.Rule("gemma-4").AsSubstring().AlsoContains("e2b").Inherits();
builder.Rule("gemma4").AsSubstring().AlsoContains("e4b").Inherits();
builder.Rule("gemma-4").AsSubstring().AlsoContains("e4b").Inherits();
builder.Rule("gemma4").AsSubstring().AlsoContains("12b").Inherits();
builder.Rule("gemma-4").AsSubstring().AlsoContains("12b").Inherits();
}
}

View File

@ -0,0 +1,62 @@
using static AIStudio.Provider.Capability;
namespace AIStudio.Models.Microsoft;
/// <summary>
/// Phi, the small Microsoft models, of which the fourth generation is the one with rules.
/// </summary>
/// <remarks>
/// What a Phi 4 checkpoint can do is written in its name, and two of those words can stand in the
/// same one. "Phi-4-mini-reasoning" is both, and the previous rules had to look for the thinking
/// first so the mini check would not claim it and state the opposite. Here the mini rule says out
/// loud that it does not speak for the thinking checkpoints, which is the same statement without an
/// order behind it.
///
/// Tool calling follows the chat template rather than the size: the mini and multimodal checkpoints
/// carry tool tokens, the 14B model has no tool role at all, and neither do the thinking ones.
/// </remarks>
public sealed class PhiFamily : ModelFamily
{
/// <inheritdoc />
public override ModelVendor Vendor => ModelVendor.MICROSOFT;
/// <inheritdoc />
public override ModelSource Source => new("https://huggingface.co/microsoft", new DateOnly(2026, 9, 11), "Ported unchanged from the Phi block of ProviderExtensions.OpenSource.cs.");
/// <inheritdoc />
protected override void Declare(ModelFamilyBuilder builder)
{
// The 14B model answers in text and has nothing to call a function with:
builder.Rule("phi4").AsSubstring()
.Capabilities(TEXT_INPUT | TEXT_OUTPUT)
.Apis(CHAT_COMPLETION_API);
builder.Rule("phi-4").AsSubstring().Inherits();
// The mini checkpoints call functions, and they are not the thinking ones:
builder.Rule("phi4").AsSubstring().AlsoContains("mini").NotContains("reasoning").Inherits()
.Capabilities(FUNCTION_CALLING);
builder.Rule("phi-4").AsSubstring().AlsoContains("mini").NotContains("reasoning").Inherits();
// The multimodal one reads pictures and listens:
builder.Rule("phi4").AsSubstring().AlsoContains("multimodal").Inherits()
.Capabilities(MULTIPLE_IMAGE_INPUT | AUDIO_INPUT);
builder.Rule("phi-4").AsSubstring().AlsoContains("multimodal").Inherits();
// The thinking checkpoints always think, and they call nothing:
builder.Rule("phi4").AsSubstring().AlsoContains("reasoning")
.Capabilities(TEXT_INPUT | TEXT_OUTPUT)
.Apis(CHAT_COMPLETION_API)
.Reasoning(ReasoningSupport.ALWAYS);
builder.Rule("phi-4").AsSubstring().AlsoContains("reasoning").Inherits();
// One of them looks at pictures while it does:
builder.Rule("phi4").AsSubstring().AlsoContains("reasoning", "vision").Inherits()
.Capabilities(MULTIPLE_IMAGE_INPUT);
builder.Rule("phi-4").AsSubstring().AlsoContains("reasoning", "vision").Inherits();
}
}

View File

@ -0,0 +1,41 @@
using static AIStudio.Provider.Capability;
namespace AIStudio.Models.NVIDIA;
/// <summary>
/// Nemotron, which NVIDIA builds for agentic work and mostly out of somebody else's weights.
/// </summary>
/// <remarks>
/// That last part is what the rules have to get right. Llama-3.3-Nemotron-Super carries two family
/// names, and the previous rules answered it as a Llama for no better reason than that the Llama
/// block stood higher up in the file. What NVIDIA changed about those weights is exactly the part
/// the answer is about: the thinking switch and the tool template. Here the name part wins over the
/// substring, so the model is answered by the family which made it what it is.
///
/// Every generation is text only. The point releases carry a line of their own because a dot
/// separates versions rather than name parts, so "nemotron-3" does not answer for "nemotron-3.5".
/// </remarks>
public sealed class NemotronFamily : ModelFamily
{
/// <inheritdoc />
public override ModelVendor Vendor => ModelVendor.NVIDIA;
/// <inheritdoc />
public override ModelSource Source => new("https://huggingface.co/nvidia", new DateOnly(2026, 9, 11), "Ported unchanged from the Nemotron block of ProviderExtensions.OpenSource.cs.");
/// <inheritdoc />
protected override void Declare(ModelFamilyBuilder builder)
{
// The earlier generations have to be asked to think:
builder.Rule("nemotron").AsSegment()
.Capabilities(TEXT_INPUT | TEXT_OUTPUT | FUNCTION_CALLING)
.Apis(CHAT_COMPLETION_API)
.Reasoning(ReasoningSupport.OPTIONAL);
// The third one thinks unless the request says otherwise, through enable_thinking=False:
builder.Rule("nemotron-3").AsSegment().Inherits()
.Reasoning(ReasoningSupport.ON_BY_DEFAULT);
builder.Rule("nemotron-3.5").AsSegment().Inherits();
}
}

View File

@ -220,7 +220,6 @@ SELF_HOSTED | kimi-k3:latest | ALWAYS_REASONING, CHAT_COMPLETION_API, FUNCTION_C
SELF_HOSTED | kimi-vl:16b | ALWAYS_REASONING, CHAT_COMPLETION_API, MULTIPLE_IMAGE_INPUT, TEXT_INPUT, TEXT_OUTPUT
SELF_HOSTED | ling-1t | CHAT_COMPLETION_API, FUNCTION_CALLING, TEXT_INPUT, TEXT_OUTPUT
SELF_HOSTED | llama-3.1-405b-base | CHAT_COMPLETION_API, TEXT_INPUT, TEXT_OUTPUT
SELF_HOSTED | llama-3.3-nemotron-super-49b | CHAT_COMPLETION_API, FUNCTION_CALLING, TEXT_INPUT, TEXT_OUTPUT
SELF_HOSTED | llama2:13b | CHAT_COMPLETION_API, TEXT_INPUT, TEXT_OUTPUT
SELF_HOSTED | llama3.2-vision:11b | CHAT_COMPLETION_API, MULTIPLE_IMAGE_INPUT, TEXT_INPUT, TEXT_OUTPUT
SELF_HOSTED | llama3.2:3b | CHAT_COMPLETION_API, FUNCTION_CALLING, TEXT_INPUT, TEXT_OUTPUT

View File

@ -134,6 +134,16 @@ public static class ExpectedChanges
Reason: "The descriptive name is recognized as a GPT model and then placed nowhere: every version rule matches the beginning of the name, which here is the list number. The model loses the reasoning it is known for.",
Source: "The GWDG entry \"gpt-5.5\" of this corpus is the same model and does report reasoning by default."),
//
// One vendor's block swallowing another vendor's model, for no reason but where the two
// blocks stand in the file.
//
new(SELF_HOSTED, "llama-3.3-nemotron-super-49b",
AnswerToday: [TEXT_INPUT, TEXT_OUTPUT, FUNCTION_CALLING, CHAT_COMPLETION_API],
AnswerWanted: [TEXT_INPUT, TEXT_OUTPUT, OPTIONAL_REASONING, FUNCTION_CALLING, CHAT_COMPLETION_API],
Reason: "An NVIDIA model is answered by the Llama rules because it carries the name of the weights it was built from, and the Llama block stands above the Nemotron one. It loses the thinking switch, which is one of the two things NVIDIA changed about those weights.",
Source: "The corpus entry \"nemotron-3-49b\" is the generation after it and does report thinking; NVIDIA documents the detailed thinking switch for the Llama-Nemotron models."),
//
// A prefix rule swallowing the variant which says the opposite.
//

View File

@ -70,6 +70,7 @@ public sealed class PortingDifferenceTests
ModelVendor.META,
ModelVendor.Z_AI,
ModelVendor.MICROSOFT,
ModelVendor.NVIDIA,
];
/// <summary>