From a0655ab408e8fb01c868d456df86f67fbf288b49 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 11 Sep 2026 08:48:50 +0200 Subject: [PATCH] Apply vendor rules to models resold under their plain names --- .../Settings/ProviderExtensions.Gateway.cs | 4 + .../Settings/ProviderExtensions.OpenSource.cs | 101 +++++++++++++----- 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.Gateway.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.Gateway.cs index bd2edbca..a09f9c45 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.Gateway.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.Gateway.cs @@ -69,6 +69,10 @@ public static partial class ProviderExtensions /// A gateway serves every model through its OpenAI-compatible chat completion API. /// The Responses API is not available there, no matter which API the original /// provider offers. + /// + /// The same holds for a provider which resells a model under its plain name instead of + /// prefixing it with the vendor, such as GWDG. Those go through the open source rules, which + /// call this for the very same reason. /// private static List NormalizeForGateway(List capabilities) { diff --git a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs index e96c0c2a..9e7652e8 100644 --- a/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs +++ b/app/MindWork AI Studio/Settings/ProviderExtensions.OpenSource.cs @@ -27,7 +27,31 @@ public static partial class ProviderExtensions // where a provider left it out, or remove one where it added it, so a family which is // written both as "llama3" and as "llama-3" still needs both spellings. // - + + // + // Some providers serve the models of the big vendors under their plain names, without the + // "vendor/model" prefix a gateway would put in front. GWDG is the case which brought this + // up: next to the open weights it hosts, it resells Claude and GPT models and names them + // the way their vendor does. A freely chosen LiteLLM alias and a self-hosted proxy can do + // the same. Without this, all of them would be judged by the rules for open weights, which + // know none of them, and would lose tool calling, vision, and reasoning alike. + // + // Only vendors whose rules do not lead back here may be asked. Mistral and DeepSeek fall + // back to this function themselves, so delegating to them would loop. + // + // Whatever comes back is normalized the way a gateway's answer is: a provider reselling a + // model serves it through its own OpenAI-compatible chat completion API, never through the + // Responses API of the vendor it bought the model from. + // + if (modelName.StartsWith("claude-") || modelName.IndexOf("-claude-") is not -1) + return NormalizeForGateway(GetModelCapabilitiesAnthropic(model)); + + if (modelName.StartsWith("gemini-") || modelName.IndexOf("-gemini-") is not -1) + return NormalizeForGateway(GetModelCapabilitiesGoogle(model)); + + if (IsOpenAICloudModelName(modelName)) + return NormalizeForGateway(GetModelCapabilitiesOpenAI(model)); + // // Meta llama models: // @@ -448,31 +472,20 @@ public static partial class ProviderExtensions } // - // OpenAI models: + // The open-weight models of OpenAI. Everything else named after an OpenAI model, the + // gpt-3.5 aliases included, was handed to their rules at the top of this function, which + // is why only gpt-oss is left here. // - if (modelName.IndexOf("gpt-oss") is not -1 || - modelName.IndexOf("gpt-3.5") is not -1) - { - if(modelName.IndexOf("gpt-oss") is not -1) - return - [ - Capability.TEXT_INPUT, - Capability.TEXT_OUTPUT, - - Capability.FUNCTION_CALLING, - Capability.WEB_SEARCH, - Capability.CHAT_COMPLETION_API, - ]; - - if(modelName.IndexOf("gpt-3.5") is not -1) - return - [ - Capability.TEXT_INPUT, - Capability.TEXT_OUTPUT, - - Capability.CHAT_COMPLETION_API, - ]; - } + if (modelName.IndexOf("gpt-oss") is not -1) + return + [ + Capability.TEXT_INPUT, + Capability.TEXT_OUTPUT, + + Capability.FUNCTION_CALLING, + Capability.WEB_SEARCH, + Capability.CHAT_COMPLETION_API, + ]; // // NVIDIA Nemotron models. They are built for agentic workloads and are text @@ -641,4 +654,42 @@ public static partial class ProviderExtensions Capability.CHAT_COMPLETION_API, ]; } + + /// + /// Checks whether a model is named after one of the models OpenAI serves through its API. + /// + /// The normalized model name. + /// True, when the name belongs to an OpenAI cloud model. + private static bool IsOpenAICloudModelName(ReadOnlySpan modelName) + { + // + // The o-series carries no vendor word at all, which is why it counts only at the very + // front of the name. Looking for it anywhere would claim open weights which end on the + // same two characters, such as Marco-o1. + // + if (modelName.StartsWith("o1") || modelName.StartsWith("o3") || modelName.StartsWith("o4")) + return true; + + if (IsVersionedGptName(modelName)) + return true; + + // + // Providers which answer with a descriptive name carry the model in the middle of it, as + // in "01 - GPT-5.5 - great overall performance": + // + var separatorIndex = modelName.IndexOf("-gpt-"); + return separatorIndex is not -1 && IsVersionedGptName(modelName[(separatorIndex + 1)..]); + } + + /// + /// Checks whether a name starts with "gpt-" followed by a version. + /// + /// + /// The digit is what separates the models OpenAI serves from the open weights which borrow + /// the name: gpt-oss, gpt-neox, and gpt-j are none of theirs. + /// + /// The normalized model name, or a part of it. + /// True, when the name starts with a versioned GPT name. + private static bool IsVersionedGptName(ReadOnlySpan modelName) => + modelName.StartsWith("gpt-") && modelName.Length > 4 && char.IsAsciiDigit(modelName[4]); } \ No newline at end of file