2025-11-13 17:13:16 +00:00
|
|
|
using AIStudio.Provider;
|
2025-05-11 10:51:35 +00:00
|
|
|
|
2025-11-13 17:13:16 +00:00
|
|
|
namespace AIStudio.Settings;
|
|
|
|
|
|
|
|
|
|
public static partial class ProviderExtensions
|
2025-05-11 10:51:35 +00:00
|
|
|
{
|
2025-12-30 17:30:32 +00:00
|
|
|
private static List<Capability> GetModelCapabilitiesOpenSource(Model model)
|
2025-05-11 10:51:35 +00:00
|
|
|
{
|
2026-09-10 19:47:36 +00:00
|
|
|
var modelName = NormalizeModelId(model.Id).AsSpan();
|
2025-05-11 10:51:35 +00:00
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// 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
|
2026-09-10 19:47:36 +00:00
|
|
|
// - 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.
|
2025-05-11 10:51:35 +00:00
|
|
|
//
|
2026-09-11 06:48:50 +00:00
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// 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));
|
|
|
|
|
|
2025-05-11 10:51:35 +00:00
|
|
|
//
|
|
|
|
|
// 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,
|
2025-09-03 08:08:04 +00:00
|
|
|
Capability.CHAT_COMPLETION_API,
|
2025-05-11 10:51:35 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// The old vision models cannot do function calling:
|
|
|
|
|
if (modelName.IndexOf("vision") is not -1)
|
2025-09-03 08:08:04 +00:00
|
|
|
return [
|
|
|
|
|
Capability.TEXT_INPUT,
|
|
|
|
|
Capability.MULTIPLE_IMAGE_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
2025-05-11 10:51:35 +00:00
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// 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,
|
2025-09-03 08:08:04 +00:00
|
|
|
Capability.CHAT_COMPLETION_API,
|
2025-05-11 10:51:35 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// All other llama models can only do text input and output:
|
2025-09-03 08:08:04 +00:00
|
|
|
return [
|
|
|
|
|
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
2025-05-11 10:51:35 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-29 14:07:00 +00:00
|
|
|
//
|
|
|
|
|
// 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,
|
|
|
|
|
];
|
|
|
|
|
|
2025-05-11 10:51:35 +00:00
|
|
|
//
|
|
|
|
|
// DeepSeek models:
|
|
|
|
|
//
|
|
|
|
|
if (modelName.IndexOf("deepseek") is not -1)
|
|
|
|
|
{
|
2026-08-12 17:02:06 +00:00
|
|
|
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,
|
|
|
|
|
];
|
|
|
|
|
|
2026-09-10 19:47:36 +00:00
|
|
|
if(modelName.IndexOf("deepseek-r1") is not -1)
|
2025-09-03 08:08:04 +00:00
|
|
|
return [
|
|
|
|
|
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
|
|
|
|
|
Capability.ALWAYS_REASONING,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
2025-05-11 10:51:35 +00:00
|
|
|
|
2025-09-03 08:08:04 +00:00
|
|
|
return [
|
|
|
|
|
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
2025-05-11 10:51:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// Qwen models:
|
|
|
|
|
//
|
|
|
|
|
if (modelName.IndexOf("qwen") is not -1 || modelName.IndexOf("qwq") is not -1)
|
|
|
|
|
{
|
|
|
|
|
if (modelName.IndexOf("qwq") is not -1)
|
2025-09-03 08:08:04 +00:00
|
|
|
return [
|
|
|
|
|
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
|
|
|
|
|
Capability.ALWAYS_REASONING,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
2026-08-12 17:02:06 +00:00
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
];
|
2026-08-19 09:53:05 +00:00
|
|
|
|
2026-08-29 14:07:00 +00:00
|
|
|
// 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,
|
|
|
|
|
];
|
|
|
|
|
|
2026-09-10 19:47:36 +00:00
|
|
|
//
|
|
|
|
|
// 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)
|
2026-08-19 09:53:05 +00:00
|
|
|
return
|
|
|
|
|
[
|
2026-09-11 06:40:40 +00:00
|
|
|
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
2026-08-19 09:53:05 +00:00
|
|
|
Capability.TEXT_OUTPUT,
|
|
|
|
|
|
|
|
|
|
Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
2026-09-11 06:40:40 +00:00
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// 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,
|
|
|
|
|
];
|
|
|
|
|
|
2026-03-11 08:59:00 +00:00
|
|
|
// Check for Qwen 3.5:
|
|
|
|
|
if(modelName.IndexOf("qwen3.5") is not -1)
|
|
|
|
|
return
|
|
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
|
|
|
|
|
2026-07-04 16:28:52 +00:00
|
|
|
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
|
2026-03-11 08:59:00 +00:00
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
|
|
|
|
|
2026-04-17 07:02:34 +00:00
|
|
|
// Check for Qwen 3.6 family:
|
|
|
|
|
if(modelName.IndexOf("qwen3.6") is not -1)
|
2026-04-15 17:03:52 +00:00
|
|
|
return
|
|
|
|
|
[
|
2026-08-12 17:02:06 +00:00
|
|
|
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
2026-04-15 17:03:52 +00:00
|
|
|
Capability.TEXT_OUTPUT,
|
|
|
|
|
|
2026-08-12 17:02:06 +00:00
|
|
|
Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING,
|
2026-04-15 17:03:52 +00:00
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
|
|
|
|
|
2025-12-30 17:30:32 +00:00
|
|
|
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,
|
|
|
|
|
];
|
|
|
|
|
|
2025-09-03 08:08:04 +00:00
|
|
|
return [
|
|
|
|
|
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
2025-05-11 10:51:35 +00:00
|
|
|
}
|
2026-08-12 17:02:06 +00:00
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// 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,
|
|
|
|
|
];
|
2025-05-11 10:51:35 +00:00
|
|
|
|
2026-08-29 14:07:00 +00:00
|
|
|
//
|
|
|
|
|
// 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,
|
|
|
|
|
];
|
|
|
|
|
|
2026-08-29 20:25:12 +00:00
|
|
|
//
|
|
|
|
|
// 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);
|
|
|
|
|
|
2025-05-11 10:51:35 +00:00
|
|
|
//
|
|
|
|
|
// Mistral models:
|
|
|
|
|
//
|
|
|
|
|
if (modelName.IndexOf("mistral") is not -1 ||
|
2025-12-03 10:05:38 +00:00
|
|
|
modelName.IndexOf("magistral") is not -1 ||
|
|
|
|
|
modelName.IndexOf("voxtral") is not -1 ||
|
2025-05-11 10:51:35 +00:00
|
|
|
modelName.IndexOf("pixtral") is not -1)
|
|
|
|
|
{
|
|
|
|
|
if(modelName.IndexOf("pixtral") is not -1)
|
|
|
|
|
return
|
|
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
2025-12-03 10:05:38 +00:00
|
|
|
|
2025-09-03 08:08:04 +00:00
|
|
|
Capability.FUNCTION_CALLING,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
2025-05-11 10:51:35 +00:00
|
|
|
];
|
|
|
|
|
|
2026-07-04 16:28:52 +00:00
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
2025-12-03 10:05:38 +00:00
|
|
|
if (modelName.IndexOf("mistral-3") is not -1 ||
|
|
|
|
|
modelName.IndexOf("mistral-large-3") is not -1)
|
|
|
|
|
return
|
|
|
|
|
[
|
2026-04-15 17:03:52 +00:00
|
|
|
Capability.TEXT_INPUT,
|
|
|
|
|
Capability.MULTIPLE_IMAGE_INPUT,
|
2025-12-03 10:05:38 +00:00
|
|
|
Capability.TEXT_OUTPUT,
|
2026-04-15 17:03:52 +00:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
2025-12-03 10:05:38 +00:00
|
|
|
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,
|
|
|
|
|
];
|
|
|
|
|
|
2025-12-30 17:30:32 +00:00
|
|
|
if (modelName.IndexOf("3.1") is not -1 ||
|
|
|
|
|
modelName.IndexOf("3.2") is not -1)
|
2025-05-11 10:51:35 +00:00
|
|
|
return
|
|
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
2025-12-03 10:05:38 +00:00
|
|
|
|
2025-09-03 08:08:04 +00:00
|
|
|
Capability.FUNCTION_CALLING,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
2025-05-11 10:51:35 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Default:
|
|
|
|
|
return
|
|
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
2025-12-03 10:05:38 +00:00
|
|
|
|
2025-09-03 08:08:04 +00:00
|
|
|
Capability.FUNCTION_CALLING,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
2025-05-11 10:51:35 +00:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// 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,
|
2025-09-03 08:08:04 +00:00
|
|
|
Capability.CHAT_COMPLETION_API,
|
2025-05-11 10:51:35 +00:00
|
|
|
];
|
|
|
|
|
|
2026-08-29 14:07:00 +00:00
|
|
|
// 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,
|
|
|
|
|
];
|
|
|
|
|
|
2025-05-11 10:51:35 +00:00
|
|
|
if(modelName.StartsWith("grok-3-mini"))
|
|
|
|
|
return
|
|
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
|
|
|
|
|
|
|
|
|
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
|
2025-09-03 08:08:04 +00:00
|
|
|
Capability.CHAT_COMPLETION_API,
|
2025-05-11 10:51:35 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if(modelName.StartsWith("grok-3"))
|
2026-08-29 14:07:00 +00:00
|
|
|
return
|
2025-05-11 10:51:35 +00:00
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
2026-08-29 14:07:00 +00:00
|
|
|
|
2025-05-11 10:51:35 +00:00
|
|
|
Capability.FUNCTION_CALLING,
|
2025-09-03 08:08:04 +00:00
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
2026-08-29 14:07:00 +00:00
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
];
|
2025-09-03 08:08:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
2026-09-11 06:48:50 +00:00
|
|
|
// 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.
|
2025-09-03 08:08:04 +00:00
|
|
|
//
|
2026-09-11 06:48:50 +00:00
|
|
|
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,
|
|
|
|
|
];
|
2025-05-11 10:51:35 +00:00
|
|
|
|
2026-08-29 14:07:00 +00:00
|
|
|
//
|
|
|
|
|
// 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
|
|
|
|
|
// NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4.
|
|
|
|
|
//
|
|
|
|
|
if (modelName.IndexOf("nemotron") is not -1)
|
|
|
|
|
return
|
|
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
|
|
|
|
|
|
|
|
|
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
|
|
|
|
|
2026-08-30 14:45:14 +00:00
|
|
|
//
|
|
|
|
|
// 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 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.
|
|
|
|
|
//
|
|
|
|
|
if (modelName.IndexOf("gemma-4") is not -1 ||
|
2026-09-10 19:47:36 +00:00
|
|
|
modelName.IndexOf("gemma4") is not -1)
|
2026-08-30 14:45:14 +00:00
|
|
|
{
|
|
|
|
|
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.VIDEO_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
|
|
|
|
|
|
|
|
|
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
|
|
|
|
Capability.VIDEO_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. 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.
|
|
|
|
|
//
|
|
|
|
|
if (modelName.IndexOf("gemma-3") is not -1 ||
|
2026-09-10 19:47:36 +00:00
|
|
|
modelName.IndexOf("gemma3") is not -1)
|
2026-08-30 14:45:14 +00:00
|
|
|
{
|
|
|
|
|
if (modelName.IndexOf("-1b") is not -1)
|
|
|
|
|
return
|
|
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
|
|
|
|
|
|
|
|
|
|
Capability.FUNCTION_CALLING,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
|
|
|
|
|
|
|
|
|
Capability.FUNCTION_CALLING,
|
|
|
|
|
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,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 17:30:32 +00:00
|
|
|
//
|
|
|
|
|
// Z AI / GLM models:
|
|
|
|
|
//
|
|
|
|
|
if (modelName.IndexOf("glm") is not -1)
|
|
|
|
|
{
|
2026-08-29 20:25:12 +00:00
|
|
|
//
|
|
|
|
|
// 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.
|
|
|
|
|
//
|
|
|
|
|
|
2026-08-29 14:07:00 +00:00
|
|
|
// 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:
|
2026-08-29 20:25:12 +00:00
|
|
|
if (modelName.IndexOf("glm-5.3") is not -1 ||
|
|
|
|
|
modelName.IndexOf("glm-5-3") is not -1)
|
2026-08-29 14:07:00 +00:00
|
|
|
return
|
|
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
|
|
|
|
|
|
|
|
|
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
|
|
|
|
|
2026-08-29 20:25:12 +00:00
|
|
|
if (modelName.IndexOf("glm-5.2") is not -1 ||
|
|
|
|
|
modelName.IndexOf("glm-5-2") is not -1)
|
2026-08-12 17:02:06 +00:00
|
|
|
return
|
|
|
|
|
[
|
|
|
|
|
Capability.TEXT_INPUT,
|
|
|
|
|
Capability.TEXT_OUTPUT,
|
|
|
|
|
|
|
|
|
|
Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
|
|
|
|
|
2025-12-30 17:30:32 +00:00
|
|
|
if(modelName.IndexOf("v") 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("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,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-11 10:51:35 +00:00
|
|
|
// Default:
|
2025-09-03 08:08:04 +00:00
|
|
|
return [
|
|
|
|
|
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
|
|
|
|
|
Capability.CHAT_COMPLETION_API,
|
|
|
|
|
];
|
2025-05-11 10:51:35 +00:00
|
|
|
}
|
2026-09-11 06:48:50 +00:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Checks whether a model is named after one of the models OpenAI serves through its API.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="modelName">The normalized model name.</param>
|
|
|
|
|
/// <returns>True, when the name belongs to an OpenAI cloud model.</returns>
|
|
|
|
|
private static bool IsOpenAICloudModelName(ReadOnlySpan<char> 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)..]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Checks whether a name starts with "gpt-" followed by a version.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// 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.
|
|
|
|
|
/// </remarks>
|
|
|
|
|
/// <param name="modelName">The normalized model name, or a part of it.</param>
|
|
|
|
|
/// <returns>True, when the name starts with a versioned GPT name.</returns>
|
|
|
|
|
private static bool IsVersionedGptName(ReadOnlySpan<char> modelName) =>
|
|
|
|
|
modelName.StartsWith("gpt-") && modelName.Length > 4 && char.IsAsciiDigit(modelName[4]);
|
2026-07-05 13:20:29 +00:00
|
|
|
}
|