using AIStudio.Provider; namespace AIStudio.Settings; public static partial class ProviderExtensions { private static List GetModelCapabilitiesOpenSource(Model model) { var modelName = NormalizeModelId(model.Id).AsSpan(); // // Checking for names in the case of open source models is a hard task. // Let's assume we want to check for the llama 3.1 405b model. // // Here is a not complete list of how providers name this model: // - Fireworks: accounts/fireworks/models/llama-v3p1-405b-instruct // - Hugging Face -> Nebius AI Studio: meta-llama/Meta-Llama-3.1-405B-Instruct // - Groq: llama-3.1-405b-instruct // - LM Studio: llama-3.1-405b-instruct // - Helmholtz Blablador: 1 - Llama3 405 the best general model // - GWDG: Llama 3.1 405B Instruct // - Ollama: llama3.1:405b // // The name arrives here already normalized by NormalizeModelId: lowercase, with every // separator written as a single hyphen. That is why the checks below no longer carry a // variant with a space or a colon. What normalization cannot do is insert a separator // 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)); // // 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: // if (modelName.IndexOf("llama") is not -1) { if (modelName.IndexOf("llama4") is not -1 || modelName.IndexOf("llama-4") is not -1 || modelName.IndexOf("llama-v4") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; // The old vision models cannot do function calling: if (modelName.IndexOf("vision") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.CHAT_COMPLETION_API, ]; // // All models >= 3.1 are able to do function calling: // if (modelName.IndexOf("llama3.") is not -1 || modelName.IndexOf("llama-3.") is not -1 || modelName.IndexOf("llama-v3p") is not -1) return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; // All other llama models can only do text input and output: return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.CHAT_COMPLETION_API, ]; } // // Meta Muse models. They need their own block because their names do not // contain "llama". Muse Glimmer always reasons: its chat template opens the // thinking channel unconditionally, only the reasoning strength can be lowered. // if (modelName.IndexOf("muse-glimmer") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, 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, Capability.ALWAYS_REASONING, Capability.CHAT_COMPLETION_API, ]; // Check for the open-weight Qwen 3.8 checkpoint: if(modelName.IndexOf("qwen3.8-2.4t-a95b") is not -1) return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; // Check for the Qwen 3.8 Flash models. The open weights are published as // Flash-Next, while Flash without the suffix is the production model. Both // share the same capabilities, so one check covers them: if(modelName.IndexOf("qwen3.8-flash") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.VIDEO_INPUT, Capability.TEXT_OUTPUT, Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; // // Check for the multimodal Qwen 3.8 27B checkpoint. Blablador writes this one in two // further ways, which no normalization can turn into the canonical name: it separates // the family from the version ("Qwen 3.8-27B with DFlash on haicluster"), and its short // alias drops the dot ("alias-qwen38-27b"). // if(modelName.IndexOf("qwen3.8-27b") is not -1 || modelName.IndexOf("qwen-3.8-27b") is not -1 || modelName.IndexOf("qwen38-27b") 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, ]; // // Any other Qwen 3.8 checkpoint. The three checks above all need a size or a variant // in the name, which the rolling tags do not carry: Ollama serves the 27B checkpoint // as "qwen3.8:latest". Without this, such a name would fall through to the generic // Qwen rule and lose everything the family can do. The 27B checkpoint is what the // rolling tag points to, so it decides what this tier promises. // if(modelName.IndexOf("qwen3.8") 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, ]; // Check for Qwen 3.5: if(modelName.IndexOf("qwen3.5") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; // Check for Qwen 3.6 family: if(modelName.IndexOf("qwen3.6") 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, ]; if(modelName.IndexOf("-vl-") is not -1) 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, ]; } // // Moonshot AI / Kimi models: // if (modelName.IndexOf("kimi-k3") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.VIDEO_INPUT, Capability.TEXT_OUTPUT, Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; if (modelName.IndexOf("kimi-k2.7-code") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, 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 // parameter defaults to no_think, low and high must be requested. We also match // the short name because providers offer the model as tencent/hy3, so checking // the start of the name is not enough. // if (modelName.IndexOf("hunyuan") is not -1 || modelName.IndexOf("hy3") is not -1) return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; // // Ministral models. They need their own block because their names do not contain // "mistral" as a substring, so the block below never sees them. Only Ministral 3 accepts // images, the 2024 models are text only, which is why the release date decides here too: // if (modelName.IndexOf("ministral") is not -1) return BuildMistralCapabilities(GetMistralReleaseDate(modelName, MINISTRAL_LATEST), MINISTRAL_VISION_SINCE, MISTRAL_REASONING_NEVER); // // Mistral models: // if (modelName.IndexOf("mistral") is not -1 || modelName.IndexOf("magistral") is not -1 || modelName.IndexOf("voxtral") is not -1 || modelName.IndexOf("pixtral") is not -1) { if(modelName.IndexOf("pixtral") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; // Mistral medium 3.5: if (modelName.IndexOf("mistral-medium-3.5") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; if (modelName.IndexOf("mistral-3") is not -1 || modelName.IndexOf("mistral-large-3") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; if (modelName.IndexOf("mistral-small-3") is not -1 || modelName.IndexOf("mistral-small-4") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; if (modelName.IndexOf("mistral-small-") is not -1) return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; if (modelName.IndexOf("voxtral-") is not -1) return [ Capability.TEXT_INPUT, Capability.SPEECH_INPUT, Capability.TEXT_OUTPUT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; // Magistral models: if (modelName.IndexOf("magistral-") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.FUNCTION_CALLING, Capability.ALWAYS_REASONING, Capability.CHAT_COMPLETION_API, ]; if (modelName.IndexOf("3.1") is not -1 || modelName.IndexOf("3.2") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; // Default: return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; } // // Grok models: // if (modelName.IndexOf("grok") is not -1) { if(modelName.IndexOf("-vision-") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.CHAT_COMPLETION_API, ]; // Grok 4 models take text, images, and video natively. Reasoning is always // on, only the reasoning effort can be configured: if(modelName.IndexOf("grok-4") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.VIDEO_INPUT, Capability.TEXT_OUTPUT, Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; if(modelName.StartsWith("grok-3-mini")) return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; if(modelName.StartsWith("grok-3")) return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; // Any other Grok model. Without this, unknown Grok versions would fall // through to the global default and would lose function calling: return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; } // // 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) 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 // 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, Capability.TEXT_OUTPUT, 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 // Gemma is handled here and Gemini in the Google implementation. // if (modelName.IndexOf("gemma") is not -1) { // // 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) { if (modelName.IndexOf("e2b") is not -1 || modelName.IndexOf("e4b") is not -1 || modelName.IndexOf("12b") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.AUDIO_INPUT, Capability.TEXT_OUTPUT, Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; } // // 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) { if (modelName.IndexOf("-1b") is not -1) return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.CHAT_COMPLETION_API, ]; 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, ]; return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.CHAT_COMPLETION_API, ]; } // // The earlier generations take text only and were not built for tool usage: // return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.CHAT_COMPLETION_API, ]; } // // Z AI / GLM models: // if (modelName.IndexOf("glm") is not -1) { // // Both version checks below accept a hyphen as the version separator as well: // Mistral serves these models as glm-5-2 and zai-glm-5-2, while everybody else // writes the version with a dot. // // GLM 5.3 uses forced thinking: the reasoning effort can be lowered, but // reasoning cannot be turned off. This check must stay in front of the // vision check below, because quantized builds such as GLM-5.3-Flash-NVFP4 // contain a "v" and would be misread as a vision model: if (modelName.IndexOf("glm-5.3") is not -1 || modelName.IndexOf("glm-5-3") is not -1) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; if (modelName.IndexOf("glm-5.2") is not -1 || modelName.IndexOf("glm-5-2") is not -1) return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; if(IsGlmVisionModelName(modelName)) return [ Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.TEXT_OUTPUT, Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING, Capability.CHAT_COMPLETION_API, ]; if (modelName.IndexOf("glm-4-") 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.FUNCTION_CALLING, Capability.OPTIONAL_REASONING, Capability.CHAT_COMPLETION_API, ]; } // Default: return [ Capability.TEXT_INPUT, Capability.TEXT_OUTPUT, Capability.CHAT_COMPLETION_API, ]; } /// /// 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. /// /// 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]); }