From 5ec1b0f3bca006660f044b9da625ebb32c35da0a Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 11 Sep 2026 09:14:25 +0200 Subject: [PATCH] Correct capabilities of known open-weight families --- .../Settings/ProviderExtensions.Alibaba.cs | 15 +- .../Settings/ProviderExtensions.OpenSource.cs | 259 ++++++++++++++---- 2 files changed, 223 insertions(+), 51 deletions(-) diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs index cb8eb76f..86944157 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs @@ -106,15 +106,22 @@ public static partial class ProviderExtensions ]; } - // QwQ models: + // + // QwQ models. What Model Studio serves under this name is qwq-plus, a commercial + // thinking-only model built on Qwen2.5. It is not the same model as the open-weight + // QwQ-32B, which the rules for open source models cover; the two only share a family + // name. Neither of them appears in Alibaba's list of models which call functions, and + // the model card of the open weights does not mention tools at all, which is why this + // states no such ability. Anybody who knows better can turn it on in the expert settings. + // if (modelName.StartsWith("qwq")) { return [ - Capability.TEXT_INPUT, + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, - - Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, + + Capability.ALWAYS_REASONING, Capability.CHAT_COMPLETION_API, ]; } diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs index 9e7652e8..a062075f 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs @@ -52,6 +52,93 @@ public static partial class ProviderExtensions if (IsOpenAICloudModelName(modelName)) return NormalizeForGateway(GetModelCapabilitiesOpenAI(model)); + // + // DeepSeek models. This block has to come before the Llama one: the R1 distills are Llama + // and Qwen checkpoints, and a name such as deepseek-r1-distill-llama-70b would otherwise + // be read as a plain Llama and lose its reasoning. + // + if (modelName.IndexOf("deepseek") is not -1) + { + // + // The distills are Llama and Qwen checkpoints fine-tuned on R1 answers. They reason, + // but they kept the chat template of the model they were built from, so none of the + // tool calling R1 itself was trained for survived. They are checked first because + // they carry "r1" in their name and would match the rule for it below: + // + if (modelName.IndexOf("distill") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + Capability.ALWAYS_REASONING, + Capability.CHAT_COMPLETION_API, + ]; + + // A base checkpoint was never instruction-tuned. It continues a text instead of + // answering, and it knows neither a chat template nor tools: + if (modelName.IndexOf("-base") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + Capability.CHAT_COMPLETION_API, + ]; + + // The V4 generation, which also covers the point releases such as V4.1, and the + // experimental checkpoint which takes images: + if (modelName.IndexOf("deepseek-v4") is not -1) + { + if (modelName.IndexOf("vision") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, + Capability.TEXT_OUTPUT, + Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + + return + [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + } + + if(modelName.IndexOf("deepseek-r1") is not -1) + return [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + + // + // From V3.1 on, the model has a thinking mode which the request turns on; V3.2 added + // tool calling inside that mode. The gateways write these two either as "deepseek-v3.1" + // or as "deepseek-chat-v3.1", so the version alone is what we look for: + // + if (modelName.IndexOf("v3.1") is not -1 || + modelName.IndexOf("v3.2") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + + // The rest of the V3 line answers directly and calls functions: + if (modelName.IndexOf("v3") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + + return [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + Capability.CHAT_COMPLETION_API, + ]; + } + // // Meta llama models: // @@ -115,39 +202,18 @@ public static partial class ProviderExtensions Capability.CHAT_COMPLETION_API, ]; - // - // DeepSeek models: - // - if (modelName.IndexOf("deepseek") is not -1) - { - if ((modelName.IndexOf("deepseek-v4-flash") is not -1 || - modelName.IndexOf("deepseek-v4-pro") is not -1) && - modelName.IndexOf("-base") is -1) - return - [ - Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, - Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING, - Capability.CHAT_COMPLETION_API, - ]; - - if(modelName.IndexOf("deepseek-r1") is not -1) - return [ - Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, - Capability.ALWAYS_REASONING, - Capability.CHAT_COMPLETION_API, - ]; - - return [ - Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, - Capability.CHAT_COMPLETION_API, - ]; - } - // // Qwen models: // if (modelName.IndexOf("qwen") is not -1 || modelName.IndexOf("qwq") is not -1) { + // + // QwQ has no tool calling. That is worth stating, because Alibaba serves a model of + // the same family name: qwq-plus is a commercial thinking-only model of theirs, while + // QwQ-32B here is the open-weight one built on Qwen2.5. They are two different models, + // and neither the model card of the open weights nor Alibaba's list of models which + // call functions mentions either of them. + // if (modelName.IndexOf("qwq") is not -1) return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, @@ -239,13 +305,20 @@ public static partial class ProviderExtensions return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, - + Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; - + + // + // Every other Qwen. The whole line calls functions, from Qwen 2.5 on, and the Coder + // checkpoints are built for exactly that. Reasoning is not promised here: the older + // generations have none, and which of the newer ones think by default differs per + // checkpoint, so the rules above name them one by one. + // return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; } @@ -272,6 +345,46 @@ public static partial class ProviderExtensions Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; + + // + // The vision checkpoint reasons, but it is the one Kimi model no vendor lists among those + // which call functions, so it does not get that ability here: + // + if (modelName.IndexOf("kimi-vl") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, + Capability.TEXT_OUTPUT, + + Capability.ALWAYS_REASONING, + Capability.CHAT_COMPLETION_API, + ]; + + // + // The rest of the Kimi line. Moonshot builds these for agentic work, and the K2 model card + // says so plainly: pass the tools with the request and the model decides on its own when + // to call them. The thinking variants say what they are in their name; the others answer + // directly. All of them take text only. + // + if (modelName.IndexOf("kimi") is not -1 || modelName.IndexOf("moonshot") is not -1) + { + if (modelName.IndexOf("thinking") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + + Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + + return + [ + Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + + Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + } // // Tencent Hunyuan models. Hy3 answers directly by default: its reasoning_effort @@ -489,11 +602,24 @@ public static partial class ProviderExtensions // // NVIDIA Nemotron models. They are built for agentic workloads and are text - // only. Reasoning has to be requested through enable_thinking, so it is - // optional. The check also covers the quantized checkpoints such as + // only. The check also covers the quantized checkpoints such as // NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4. // if (modelName.IndexOf("nemotron") is not -1) + { + // The third generation thinks unless the request says otherwise, through + // enable_thinking=False: + if (modelName.IndexOf("nemotron-3") is not -1) + return + [ + Capability.TEXT_INPUT, + Capability.TEXT_OUTPUT, + + Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING, + Capability.CHAT_COMPLETION_API, + ]; + + // The earlier ones have to be asked to think: return [ Capability.TEXT_INPUT, @@ -502,6 +628,7 @@ public static partial class ProviderExtensions Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; + } // // Google Gemma models. Gemma is the open-weights family, while Gemini is not, which is why @@ -510,10 +637,15 @@ public static partial class ProviderExtensions if (modelName.IndexOf("gemma") is not -1) { // - // Every checkpoint of the Gemma 4 generation is multimodal and understands video as - // well; there is no text-only variant. Audio input is limited to the E2B, E4B, and 12B - // checkpoints. The models can think, but only when the request asks them to: their chat - // template keeps the thinking channel closed by default. + // Every checkpoint of the Gemma 4 generation is multimodal; there is no text-only + // variant. Audio input is limited to the E2B, E4B, and 12B checkpoints. Video is not + // a modality of any of them: the model card lists text, image, and audio, and mentions + // video only as a sequence of frames somebody else has to cut it into. The models can + // think, but only when the request asks them to, by putting a think token at the start + // of the system prompt. + // + // Gemma 4 is also the first generation with tool calling of its own, with tool tokens + // in its chat template. The generations below have none. // if (modelName.IndexOf("gemma-4") is not -1 || modelName.IndexOf("gemma4") is not -1) @@ -524,7 +656,7 @@ public static partial class ProviderExtensions return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, - Capability.AUDIO_INPUT, Capability.VIDEO_INPUT, + Capability.AUDIO_INPUT, Capability.TEXT_OUTPUT, Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, @@ -534,7 +666,6 @@ public static partial class ProviderExtensions return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, - Capability.VIDEO_INPUT, Capability.TEXT_OUTPUT, Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, @@ -543,9 +674,17 @@ public static partial class ProviderExtensions } // - // Gemma 3 accepts images from the 4B checkpoint upwards; the 1B one is text-only. This - // generation does not reason. The check for the small checkpoint looks for "-1b" rather - // than "1b", so that a name such as gemma-3-31b does not match it. + // Gemma 3 accepts images from the 4B checkpoint upwards; the 1B one is text-only, and + // the 3n checkpoints take audio on top. This generation does not reason. + // + // It does not call functions either. What Google documents for Gemma 3 is writing the + // tool descriptions into the prompt by hand, which is a different thing from what an + // OpenAI-compatible tools field does: the chat template has neither a tool role nor + // tool tokens, and Ollama refuses a request carrying tools for these models. Native + // tool calling starts with Gemma 4 above. + // + // The check for the small checkpoint looks for "-1b" rather than "1b", so that a name + // such as gemma-3-31b does not match it. // if (modelName.IndexOf("gemma-3") is not -1 || modelName.IndexOf("gemma3") is not -1) @@ -554,8 +693,16 @@ public static partial class ProviderExtensions return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, + Capability.CHAT_COMPLETION_API, + ]; - Capability.FUNCTION_CALLING, + if (modelName.IndexOf("gemma-3n") is not -1 || + modelName.IndexOf("gemma3n") is not -1) + return + [ + Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, + Capability.AUDIO_INPUT, + Capability.TEXT_OUTPUT, Capability.CHAT_COMPLETION_API, ]; @@ -563,8 +710,6 @@ public static partial class ProviderExtensions [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, - - Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; } @@ -616,12 +761,12 @@ public static partial class ProviderExtensions Capability.CHAT_COMPLETION_API, ]; - if(modelName.IndexOf("v") is not -1) - return + if(IsGlmVisionModelName(modelName)) + return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, - + Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, @@ -655,6 +800,26 @@ public static partial class ProviderExtensions ]; } + /// + /// Checks whether a GLM model is one of the vision models. + /// + /// + /// Z AI marks these by appending a "v" to the version number: glm-4v, glm-4.1v, glm-4.5v. + /// Looking for a bare "v" anywhere in the name, which is what this used to do, calls every + /// quantized build a vision model, because "nvfp4" carries one too, and so does the name of + /// more than one inference provider. + /// + /// The normalized model name. + /// True, when the version number is followed by a "v". + private static bool IsGlmVisionModelName(ReadOnlySpan modelName) + { + for (var index = 1; index < modelName.Length; index++) + if (modelName[index] is 'v' && char.IsAsciiDigit(modelName[index - 1])) + return true; + + return false; + } + /// /// Checks whether a model is named after one of the models OpenAI serves through its API. ///