Fixed model capabilities and how model names are matched (#958)

This commit is contained in:
Thorsten Sommer 2026-09-11 15:24:02 +02:00 committed by GitHub
parent e64c51bd57
commit 2834529753
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1164 additions and 155 deletions

View File

@ -9937,6 +9937,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2337053319"] = "The provider
-- The embedding request to the provider '{0}' failed: {1}
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2423374763"] = "The embedding request to the provider '{0}' failed: {1}"
-- The selected model is not able to use tools. Please select a model which can, or open the settings of the provider '{0}', show its expert settings, and switch the function calling capability off there.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T265391888"] = "The selected model is not able to use tools. Please select a model which can, or open the settings of the provider '{0}', show its expert settings, and switch the function calling capability off there."
-- The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T2819996431"] = "The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again."

View File

@ -115,7 +115,7 @@ CONFIG["LLM_PROVIDERS"] = {}
-- -- AUDIO_INPUT, FUNCTION_CALLING, MULTIPLE_IMAGE_INPUT, SPEECH_INPUT, VIDEO_INPUT,
-- -- OPTIONAL_REASONING, ALWAYS_REASONING, REASONING_BY_DEFAULT
-- -- Allowed values are booleans only.
-- -- For default-on reasoning (rhinking), set OPTIONAL_REASONING and REASONING_BY_DEFAULT to true.
-- -- For default-on reasoning (thinking), set OPTIONAL_REASONING and REASONING_BY_DEFAULT to true.
-- -- ALWAYS_REASONING means the model cannot disable reasoning (thinking).
-- -- Missing keys keep the automatic capability detection result.
-- -- ["CapabilityOverrides"] = {

View File

@ -243,6 +243,7 @@ public abstract class BaseProvider : IProvider, ISecretId
ProviderRequestFailureReason.PROVIDER_UNAVAILABLE => string.Format(TB("The provider '{0}' could not be reached. Please check whether it is running and reachable, then try again."), this.InstanceName),
ProviderRequestFailureReason.MODEL_NOT_FOUND => string.Format(TB("The provider '{0}' does not know the selected model. Please select another model."), this.InstanceName),
ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED => TB("The text was longer than the selected model accepts. Please select a model which takes longer texts, or reduce the chunk size of the data source."),
ProviderRequestFailureReason.TOOLS_NOT_SUPPORTED => string.Format(TB("The selected model is not able to use tools. Please select a model which can, or open the settings of the provider '{0}', show its expert settings, and switch the function calling capability off there."), this.InstanceName),
ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED => string.Format(TB("The provider '{0}' cannot create embeddings. Please select a provider which offers an embedding model."), this.InstanceName),
ProviderRequestFailureReason.INVALID_RESPONSE => string.Format(TB("The provider '{0}' sent an answer AI Studio was not able to read."), this.InstanceName),
_ => string.Empty,
@ -340,6 +341,9 @@ public abstract class BaseProvider : IProvider, ISecretId
protected virtual ProviderRequestFailureReason ClassifyProviderRequestFailure(HttpStatusCode statusCode, string responseBody)
{
if (statusCode is HttpStatusCode.BadRequest && IsToolsNotSupportedFailure(responseBody))
return ProviderRequestFailureReason.TOOLS_NOT_SUPPORTED;
if (statusCode is not HttpStatusCode.TooManyRequests)
return ProviderRequestFailureReason.NONE;
@ -351,9 +355,80 @@ public abstract class BaseProvider : IProvider, ISecretId
if (IsTooManyRequestsError(errorCode) || IsTooManyRequestsError(errorType) || IsTooManyRequestsError(errorMessage))
return ProviderRequestFailureReason.TOO_MANY_REQUESTS;
//
// Some providers do not refuse the request outright, they open the stream and put the
// refusal into the first event. It is the same failure, so it gets the same answer:
//
if (IsToolsNotSupportedFailure(errorMessage) || IsToolsNotSupportedFailure(responseBody))
return ProviderRequestFailureReason.TOOLS_NOT_SUPPORTED;
return ProviderRequestFailureReason.NONE;
}
//
// The words a provider uses for the ability to call tools, and the words it uses to deny an
// ability. Neither list is complete, and neither can be: every provider words this in its own
// way. Ollama says "<model> does not support tools", Mistral "Function calling is not enabled
// for this model", others again something else. What they have in common is one word from each
// of these two lists.
//
private static readonly string[] TOOL_CALLING_WORDS = ["tool", "function call", "function_call", "function-call", "functions"];
private static readonly string[] ABILITY_DENIALS = ["not support", "unsupported", "not enabled", "not available", "not allowed", "not capable", "no support", "not implemented"];
//
// How far apart the two words may stand and still be read as one statement. The distance is
// what makes the check trustworthy: a provider which quotes the failed request back sends our
// whole tool list along with the error, so the word "tool" is then in the body no matter what
// actually went wrong. A denial elsewhere in such a body says nothing about tool calling.
//
private const int TOOL_DENIAL_MAX_DISTANCE = 60;
/// <summary>
/// Recognizes the answer a provider gives when the model cannot use the tools we offered it.
/// </summary>
/// <remarks>
/// There is no error code for this either, which is why this reads the wording like the
/// context length check above does. AI Studio needs to recognize it because it assumes tool
/// calling for models it does not know: without this, the user would see nothing but the raw
/// provider message and no hint at what to do about it.
/// </remarks>
/// <param name="responseBody">What the provider said about the failure.</param>
/// <returns>True, when the provider denied the ability to call tools.</returns>
private static bool IsToolsNotSupportedFailure(string? responseBody)
{
if (string.IsNullOrWhiteSpace(responseBody))
return false;
foreach (var denial in ABILITY_DENIALS)
{
var denialIndex = responseBody.IndexOf(denial, StringComparison.OrdinalIgnoreCase);
while (denialIndex is not -1)
{
if (MentionsToolCallingNearby(responseBody, denialIndex, denial.Length))
return true;
// The same denial may appear again later in the body, next to the tool words:
denialIndex = responseBody.IndexOf(denial, denialIndex + 1, StringComparison.OrdinalIgnoreCase);
}
}
return false;
}
private static bool MentionsToolCallingNearby(string responseBody, int denialIndex, int denialLength)
{
var windowStart = Math.Max(0, denialIndex - TOOL_DENIAL_MAX_DISTANCE);
var windowEnd = Math.Min(responseBody.Length, denialIndex + denialLength + TOOL_DENIAL_MAX_DISTANCE);
var window = responseBody.AsSpan(windowStart, windowEnd - windowStart);
foreach (var word in TOOL_CALLING_WORDS)
if (window.Contains(word, StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
private static bool IsTooManyRequestsError(string? value)
{
if (string.IsNullOrWhiteSpace(value))

View File

@ -46,7 +46,16 @@ public static class ModelKindExtensions
private static readonly string[] IMAGE_GENERATION_MARKERS = ["flux", "stable-diffusion", "sdxl", "dall-e", "midjourney", "gpt-image"];
private static readonly string[] VIDEO_GENERATION_MARKERS = ["sora", "veo-", "runway"];
//
// Google names its image models after the chat model they grew out of and appends "image":
// gemini-3-pro-image, gemini-3.1-flash-image, gemini-2.5-flash-image. Read as a plain substring,
// that word is too greedy -- it also sits inside "imagenet" and "reimagined", and a chat model
// carrying such a word would disappear from the user's list. It therefore counts only where a
// name segment begins and ends with it.
//
private static readonly string[] IMAGE_GENERATION_WORD_MARKERS = ["image"];
private static readonly string[] VIDEO_GENERATION_MARKERS = ["sora", "veo-", "runway", "hailuo"];
//
// Markers which have to stand as a word of their own. "kling" is such a case: taken as a plain
@ -107,7 +116,7 @@ public static class ModelKindExtensions
if (HasAnyMarker(model.Id, TEXT_COMPLETION_MARKERS))
return ModelKind.TEXT_COMPLETION;
if (HasAnyMarker(model.Id, IMAGE_GENERATION_MARKERS))
if (HasAnyMarker(model.Id, IMAGE_GENERATION_MARKERS) || HasAnyWordMarker(model.Id, IMAGE_GENERATION_WORD_MARKERS))
return ModelKind.IMAGE_GENERATION;
if (HasAnyMarker(model.Id, VIDEO_GENERATION_MARKERS) || HasAnyWordMarker(model.Id, VIDEO_GENERATION_WORD_MARKERS))

View File

@ -48,6 +48,16 @@ public enum ProviderRequestFailureReason
/// </summary>
CONTEXT_LENGTH_EXCEEDED,
/// <summary>
/// The request offered the model some tools, and the model cannot use them.
/// </summary>
/// <remarks>
/// AI Studio assumes that a model it has never heard of is able to call tools. Most of them
/// are, and new ones keep appearing faster than any list can follow. The few which are not
/// say so when they are asked, and this is that answer.
/// </remarks>
TOOLS_NOT_SUPPORTED,
/// <summary>
/// The provider cannot create embeddings at all.
/// </summary>

View File

@ -6,13 +6,29 @@ public static partial class ProviderExtensions
{
private static List<Capability> GetModelCapabilitiesAlibaba(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
var modelName = NormalizeModelId(model.Id).AsSpan();
// Qwen models:
if (modelName.StartsWith("qwen"))
{
// Check for omni models:
// Check for omni models. Alibaba lists the Qwen3 and Qwen3.5 Omni series among the
// models which call functions; the older qwen-omni ones are not on that list, which
// is what the version check separates here:
if (modelName.IndexOf("omni") is not -1)
{
if (modelName.StartsWith("qwen3"))
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.AUDIO_INPUT, Capability.SPEECH_INPUT,
Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT, Capability.SPEECH_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
@ -23,6 +39,7 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
}
// Check for Qwen 3.5:
if(modelName.StartsWith("qwen3.5"))
@ -47,6 +64,44 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
// Check for the Qwen 3.7 family. Thinking is optional here and switched on by
// default, except for the two preview snapshots, which do nothing else:
if(modelName.StartsWith("qwen3.7"))
{
if(modelName.IndexOf("-preview") is not -1 ||
modelName.IndexOf("-2026-05-17") is not -1)
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Vision arrived in the middle of the series. The rolling qwen3.7-max alias
// still answers as the text-only May snapshot, so only the June one may be
// told that it reads images and video:
if(modelName.IndexOf("-2026-06-08") 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,
];
return
[
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
// Check for the Qwen 3.8 family:
if(modelName.StartsWith("qwen3.8"))
{
@ -84,8 +139,20 @@ public static partial class ProviderExtensions
];
}
// Check for the 3.0 VL models:
// Check for the VL models. Alibaba names the Qwen3-VL Plus and Flash series as
// function callers; the older qwen-vl models are absent from that list:
if(modelName.IndexOf("-vl-") is not -1)
{
if(modelName.StartsWith("qwen3"))
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
@ -93,6 +160,7 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
}
// Check for Qwen 3:
if(modelName.StartsWith("qwen3"))
@ -106,7 +174,14 @@ 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
@ -114,7 +189,7 @@ public static partial class ProviderExtensions
Capability.TEXT_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.ALWAYS_REASONING,
Capability.CHAT_COMPLETION_API,
];
}

View File

@ -6,7 +6,7 @@ public static partial class ProviderExtensions
{
private static List<Capability> GetModelCapabilitiesAnthropic(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
var modelName = NormalizeModelId(model.Id).AsSpan();
// Claude Fable 5 and Mythos 5 always use adaptive thinking:
if(modelName.StartsWith("claude-fable-5") || modelName.StartsWith("claude-mythos-5"))

View File

@ -6,7 +6,7 @@ public static partial class ProviderExtensions
{
private static List<Capability> GetModelCapabilitiesDeepSeek(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
var modelName = NormalizeModelId(model.Id).AsSpan();
// The reasoner alias points to the thinking mode of the current flash model:
if(modelName.IndexOf("reasoner") is not -1)

View File

@ -32,7 +32,7 @@ public static partial class ProviderExtensions
var separatorIndex = model.Id.IndexOf('/');
var vendor = separatorIndex is -1 ? string.Empty : model.Id[..separatorIndex].ToLowerInvariant();
var bareModel = separatorIndex is -1 ? model : model with { Id = model.Id[(separatorIndex + 1)..] };
var bareModelName = bareModel.Id.ToLowerInvariant().AsSpan();
var bareModelName = NormalizeModelId(bareModel.Id).AsSpan();
var capabilities = vendor switch
{
@ -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.
/// </remarks>
private static List<Capability> NormalizeForGateway(List<Capability> capabilities)
{

View File

@ -6,14 +6,61 @@ public static partial class ProviderExtensions
{
private static List<Capability> GetModelCapabilitiesGoogle(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
var modelName = NormalizeModelId(model.Id).AsSpan();
if (modelName.IndexOf("gemini-") is not -1)
{
//
// Image generation models. They carry a version number like every other model and
// have to be asked about first, or gemini-3-pro-image would be read as a chat model
// of the 3.x line and be promised function calling. No image model of the family
// offers that; what they do offer, and the chat models do not, is writing images.
//
if (modelName.IndexOf("-image") is not -1)
{
// Of the image models, only the 3.1 Flash ones read video. They think about
// complex prompts, and, as with the 3.x chat models, thinking cannot be
// switched off:
if (modelName.IndexOf("gemini-3.1-flash") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT, Capability.IMAGE_OUTPUT,
Capability.ALWAYS_REASONING,
Capability.CHAT_COMPLETION_API,
];
// Every other Gemini 3 image model thinks as well, it just does not read video:
if (modelName.IndexOf("gemini-3") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT, Capability.IMAGE_OUTPUT,
Capability.ALWAYS_REASONING,
Capability.CHAT_COMPLETION_API,
];
// The older image models, such as the 2.5 Flash one, do not think:
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT, Capability.IMAGE_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
// Chat-compatible Gemini 3.x reasoning models. We match the entire 3.x line
// so that new releases are covered as well: they all reason, and the
// thinking level can only be lowered, never turned off. The two rolling
// aliases carry no version number and are listed separately:
// thinking level can only be lowered, never turned off. That holds for the
// Flash Lite models of this line too, which is what sets them apart from
// Gemini 2.5 Flash Lite below: there, thinking is off until it is asked for,
// while here the lowest level still thinks. The two rolling aliases carry no
// version number and are listed separately:
if (modelName.IndexOf("gemini-3") is not -1 ||
modelName is "gemini-flash-latest" ||
modelName is "gemini-pro-latest")
@ -54,17 +101,6 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
// Image generation:
if(modelName.IndexOf("-2.0-flash-preview-image-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.AUDIO_INPUT,
Capability.SPEECH_INPUT, Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT, Capability.IMAGE_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
// Realtime model:
if(modelName.IndexOf("-2.0-flash-live-") is not -1)
return
@ -78,16 +114,16 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
// The 2.0 flash models cannot call functions:
if(modelName.IndexOf("-2.0-flash-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.AUDIO_INPUT,
Capability.SPEECH_INPUT, Capability.VIDEO_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
//
// There used to be a branch here which withheld function calling from the 2.0 Flash
// models. It said the wrong thing about them, and it only ever caught the dated IDs
// because it asked for a trailing hyphen: the plain gemini-2.0-flash alias walked
// past it and got a different answer than gemini-2.0-flash-001, which is the same
// model. Both questions are moot now, because Google shut the 2.0 Flash chat models
// down on 1 June 2026. Anything still asking for one of those names gets the default
// below. The live model above keeps its branch: it belongs to a different API whose
// retirement Google announces separately.
//
// The old 1.0 pro vision model:
if(modelName.IndexOf("pro-vision") is not -1)

View File

@ -64,7 +64,7 @@ public static partial class ProviderExtensions
private static List<Capability> GetModelCapabilitiesMistral(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
var modelName = NormalizeModelId(model.Id).AsSpan();
// Pixtral models are able to do process images:
if (modelName.IndexOf("pixtral") is not -1)

View File

@ -6,7 +6,7 @@ public static partial class ProviderExtensions
{
private static List<Capability> GetModelCapabilitiesOpenAI(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
var modelName = NormalizeModelId(model.Id).AsSpan();
if (modelName is "gpt-4o-search-preview")
return
@ -54,14 +54,6 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
if (modelName.StartsWith("chatgpt-4o-"))
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.RESPONSES_API,
];
if (modelName.StartsWith("o3-mini"))
return
[
@ -118,6 +110,7 @@ public static partial class ProviderExtensions
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING, Capability.ALWAYS_REASONING,
Capability.WEB_SEARCH,
Capability.RESPONSES_API,
];
@ -132,11 +125,18 @@ public static partial class ProviderExtensions
Capability.RESPONSES_API,
];
//
// None of the GPT-5 models writes images itself. They can ask for one through the
// image generation tool, which is a tool call like any other and produces a picture
// from a separate model. That is a different thing from an output modality, and we
// must not report it as one: the chat would then offer to receive images which never
// arrive.
//
if(modelName is "gpt-5.1" || modelName.StartsWith("gpt-5.1-"))
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT, Capability.IMAGE_OUTPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING, Capability.OPTIONAL_REASONING,
Capability.WEB_SEARCH,
@ -147,7 +147,7 @@ public static partial class ProviderExtensions
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT, Capability.IMAGE_OUTPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING, Capability.OPTIONAL_REASONING,
Capability.WEB_SEARCH,
@ -180,7 +180,7 @@ public static partial class ProviderExtensions
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT, Capability.IMAGE_OUTPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING, Capability.REASONING_BY_DEFAULT,
Capability.WEB_SEARCH,
@ -198,6 +198,21 @@ public static partial class ProviderExtensions
Capability.RESPONSES_API, Capability.CHAT_COMPLETION_API,
];
//
// GPT-6 Astra. Unlike the 5.5 and 5.6 models, it reasons on every request: the effort
// reaches from low to max, and there is no setting which switches thinking off.
//
if(modelName is "gpt-6-astra" || modelName.StartsWith("gpt-6-astra-"))
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING, Capability.ALWAYS_REASONING,
Capability.WEB_SEARCH,
Capability.RESPONSES_API, Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,

View File

@ -6,7 +6,7 @@ public static partial class ProviderExtensions
{
private static List<Capability> GetModelCapabilitiesOpenSource(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
var modelName = NormalizeModelId(model.Id).AsSpan();
//
// Checking for names in the case of open source models is a hard task.
@ -19,7 +19,132 @@ public static partial class ProviderExtensions
// - 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));
//
// Base checkpoints, whatever family they come from. They were never instruction-tuned:
// they continue a text instead of answering, and they know neither a chat template nor
// tools. This is checked before any family, because otherwise each of them would have to
// repeat it, and because the default at the end of this function assumes tool calling.
//
// The name part has to be exactly "base", so that a model whose name merely contains the
// word, as in "based", is left alone.
//
if (modelName.EndsWith("-base") || modelName.IndexOf("-base-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
//
// 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,
];
// 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:
@ -27,7 +152,6 @@ public static partial class ProviderExtensions
if (modelName.IndexOf("llama") is not -1)
{
if (modelName.IndexOf("llama4") is not -1 ||
modelName.IndexOf("llama 4") is not -1 ||
modelName.IndexOf("llama-4") is not -1 ||
modelName.IndexOf("llama-v4") is not -1)
return
@ -52,7 +176,6 @@ public static partial class ProviderExtensions
// 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-3.") is not -1 ||
modelName.IndexOf("llama-v3p") is not -1)
return
@ -86,40 +209,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 ||
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,
@ -150,11 +251,35 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
// Check for the multimodal Qwen 3.8 27B checkpoint:
if(modelName.IndexOf("qwen3.8-27b") is not -1)
//
// 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.VIDEO_INPUT,
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,
@ -192,8 +317,15 @@ public static partial class ProviderExtensions
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,
];
}
@ -221,6 +353,46 @@ public static partial class ProviderExtensions
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
@ -375,12 +547,27 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
// Grok 4 models take text, images, and video natively. Reasoning is always
// on, only the reasoning effort can be configured:
// One member of the 4.20 line answers without thinking, and it says so in its
// name. It has to be asked about before the general Grok 4 rule, which would
// otherwise claim the opposite of what the name states:
if(modelName.IndexOf("-non-reasoning") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
// Grok 4 models take text and images. Reasoning is always on, only the
// reasoning effort can be configured. Video is not among their modalities:
// xAI serves audio, image, and video through models and APIs of their own,
// and the model pages of the 4.x line say "text, image" and nothing else:
if(modelName.IndexOf("grok-4") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT, Capability.VIDEO_INPUT,
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
@ -420,11 +607,10 @@ 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
[
@ -436,23 +622,26 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
if(modelName.IndexOf("gpt-3.5") is not -1)
//
// 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,
];
}
//
// 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)
// The earlier ones have to be asked to think:
return
[
Capability.TEXT_INPUT,
@ -461,6 +650,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
@ -469,13 +659,17 @@ 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 ||
modelName.IndexOf("gemma4") is not -1)
{
if (modelName.IndexOf("e2b") is not -1 ||
@ -484,7 +678,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,
@ -494,7 +688,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,
@ -503,20 +696,35 @@ 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 ||
modelName.IndexOf("gemma3") is not -1)
{
if (modelName.IndexOf("-1b") is not -1)
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,
];
@ -524,8 +732,6 @@ public static partial class ProviderExtensions
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
@ -577,7 +783,7 @@ public static partial class ProviderExtensions
Capability.CHAT_COMPLETION_API,
];
if(modelName.IndexOf("v") is not -1)
if(IsGlmVisionModelName(modelName))
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
@ -609,10 +815,518 @@ public static partial class ProviderExtensions
];
}
// Default:
return [
//
// MiniMax models. The M line is built for agentic work and thinks between its tool calls,
// which MiniMax calls interleaved thinking: the reasoning is part of the answer rather
// than something the request switches on. The older Text-01 answers directly.
//
if (modelName.IndexOf("minimax") is not -1)
{
if (modelName.IndexOf("minimax-m") 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,
];
}
//
// IBM Granite models. The instruct line calls functions using the OpenAI function
// definition schema. From 4.2 on they think unless the request says otherwise; 3.2 and 3.3
// have a thinking toggle which starts off, and the generations between them do not reason
// at all. For the vision checkpoints, tool calling is not documented.
//
if (modelName.IndexOf("granite") 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,
];
if (modelName.IndexOf("granite-4.2") is not -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if (modelName.IndexOf("granite-3.2") is not -1 ||
modelName.IndexOf("granite-3.3") is not -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
//
// Cohere Command models. Most of the line calls functions, in one step and in several.
// Command A Vision is the exception Cohere states outright: tool use is not supported
// with it.
//
if (modelName.IndexOf("command-a") is not -1 ||
modelName.IndexOf("command-r") is not -1)
{
if (modelName.IndexOf("command-a-vision") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
// Command A+ sees and thinks unless the request disables thinking:
if (modelName.IndexOf("command-a-plus") 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("command-a-reasoning") is not -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
//
// The Aya models come from Cohere as well, but they were not trained with tool use in
// mind, which their documentation says in as many words:
//
if (modelName.IndexOf("aya-expanse") is not -1 ||
modelName.IndexOf("aya-vision") is not -1)
{
if (modelName.IndexOf("aya-vision") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
//
// AI2 OLMo models. The instruct checkpoints of the third generation carry a functions
// section in their chat template, and their default system prompt calls the model a
// function-calling assistant. The Think variants reason on top of that. OLMo 2 has no
// tool template.
//
if (modelName.IndexOf("olmo") is not -1)
{
if (modelName.IndexOf("olmo-3") is not -1 || modelName.IndexOf("olmo3") is not -1)
{
if (modelName.IndexOf("think") 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,
];
}
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
//
// ByteDance Seed-OSS. Trained for agentic work, and it thinks with a budget the request
// can cap; the thinking itself cannot be turned off.
//
if (modelName.IndexOf("seed-oss") is not -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
//
// TII Falcon. The third generation was post-trained on function calls and reports its
// tool-calling benchmark in its own model card. Falcon-H1 documents no tool template,
// except for the small checkpoint built for nothing else.
//
if (modelName.IndexOf("falcon") is not -1)
{
if (modelName.IndexOf("falcon-h1") is not -1 &&
modelName.IndexOf("tool-calling") is -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
//
// InclusionAI Ling and Ring. Both call functions natively; Ring is the thinking line of
// the two, Ling the one which answers directly. Their names are only accepted where a
// name part begins, because "ling" also sits inside unrelated models such as Starling.
//
if (modelName.IndexOf("inclusionai") is not -1 ||
modelName.StartsWith("ling-") || modelName.IndexOf("-ling-") is not -1 ||
modelName.StartsWith("ring-") || modelName.IndexOf("-ring-") is not -1)
{
if (modelName.StartsWith("ring-") || modelName.IndexOf("-ring-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.REASONING_BY_DEFAULT, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
//
// Baidu ERNIE. The thinking checkpoints call functions. The vision ones run in a thinking
// and a non-thinking mode, and tool calling is not documented for them.
//
if (modelName.IndexOf("ernie") is not -1)
{
if (modelName.IndexOf("-vl") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING,
Capability.CHAT_COMPLETION_API,
];
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,
];
}
//
// Hugging Face SmolLM. The third generation ships a chat template with tool support and a
// thinking mode the request switches on. The earlier ones have neither.
//
if (modelName.IndexOf("smollm") is not -1)
{
if (modelName.IndexOf("smollm3") is not -1 || modelName.IndexOf("smollm-3") is not -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
}
//
// ServiceNow Apriel. The Thinker models see and always reason, because their default chat
// template opens the thinking channel. Tool tokens arrived with 1.6; 1.5 has none.
//
if (modelName.IndexOf("apriel") is not -1)
{
if (modelName.IndexOf("apriel-1.5") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
//
// InternLM, and the InternVL family next to it. InternLM has a role of its own for tool
// answers in its chat template and a deep-thinking mode the request asks for. What is
// documented for InternVL is that it takes images.
//
if (modelName.IndexOf("internvl") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
if (modelName.IndexOf("internlm") is not -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.OPTIONAL_REASONING, Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
//
// Swiss AI Apertus 1.5. It calls functions in the OpenAI format, takes images and audio,
// and thinks when asked to. Note that its tool calling does not work while it thinks --
// a combination these capabilities cannot express, so both are stated side by side.
//
if (modelName.IndexOf("apertus-v1.5") is not -1 ||
modelName.IndexOf("apertus-1.5") 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,
];
//
// Microsoft Phi. The mini and multimodal checkpoints of the fourth generation call
// functions with tool tokens of their own. The 14B model has no tool role in its chat
// template at all, and neither do the reasoning checkpoints, which always think.
//
if (modelName.IndexOf("phi-4") is not -1 || modelName.IndexOf("phi4") is not -1)
{
if (modelName.IndexOf("multimodal") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.AUDIO_INPUT,
Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
//
// The reasoning checkpoints have to be checked before the mini one, because
// Phi-4-mini-reasoning is both and would otherwise be read as a mini model which
// does not think:
//
if (modelName.IndexOf("reasoning") is not -1)
{
if (modelName.IndexOf("vision") is not -1)
return
[
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING,
Capability.CHAT_COMPLETION_API,
];
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.ALWAYS_REASONING,
Capability.CHAT_COMPLETION_API,
];
}
if (modelName.IndexOf("mini") 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,
];
}
//
// The models we know do not call functions. They have to be named one by one, because the
// default below assumes that an unknown model does. None of these documents a tool
// template: the European and Spanish public models, the discontinued Occiglot, and the Yi
// line, whose open weights speak plain ChatML while only the closed Yi-Large-FC calls
// functions. Salamandra is the one with a variant built for it, which keeps its ability.
//
// The family names are only accepted where a name part begins, so that "yi" does not
// match every model which happens to contain those two letters.
//
if (modelName.IndexOf("teuken") is not -1 ||
modelName.IndexOf("eurollm") is not -1 ||
modelName.IndexOf("occiglot") is not -1 ||
(modelName.IndexOf("salamandra") is not -1 && modelName.IndexOf("-tools") is -1) ||
modelName.StartsWith("yi-") || modelName.IndexOf("-yi-") is not -1)
return
[
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.CHAT_COMPLETION_API,
];
//
// Default. A model we do not recognize is assumed to call functions, because by now that
// is what an instruction-tuned model does: every family added here in the last while
// could do it, and the ones which cannot are the exception listed above. Guessing the
// other way around was the safer choice while the ability was only shown as an icon, but
// it stopped being safe once it decides whether tools are offered at all -- a model which
// can use them would silently never be asked to.
//
// Anybody hitting the rare case where this guess is wrong turns tool calling off for that
// provider in the expert settings, and an organization can do the same for everybody.
//
return [
Capability.TEXT_INPUT, Capability.TEXT_OUTPUT,
Capability.FUNCTION_CALLING,
Capability.CHAT_COMPLETION_API,
];
}
/// <summary>
/// Checks whether a GLM model is one of the vision models.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="modelName">The normalized model name.</param>
/// <returns>True, when the version number is followed by a "v".</returns>
private static bool IsGlmVisionModelName(ReadOnlySpan<char> modelName)
{
for (var index = 1; index < modelName.Length; index++)
if (modelName[index] is 'v' && char.IsAsciiDigit(modelName[index - 1]))
return true;
return false;
}
/// <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]);
}

View File

@ -6,8 +6,13 @@ public static partial class ProviderExtensions
{
private static List<Capability> GetModelCapabilitiesPerplexity(Model model)
{
var modelName = model.Id.ToLowerInvariant().AsSpan();
var modelName = NormalizeModelId(model.Id).AsSpan();
//
// No Sonar model writes images. What looked like it does is the option to have the
// answer come with images: those are pictures the search found on the pages it read,
// handed back as links, not something the model drew.
//
if(modelName.IndexOf("reasoning") is not -1 ||
modelName.IndexOf("deep-research") is not -1)
return
@ -16,7 +21,6 @@ public static partial class ProviderExtensions
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.IMAGE_OUTPUT,
Capability.ALWAYS_REASONING,
Capability.WEB_SEARCH,
@ -29,7 +33,6 @@ public static partial class ProviderExtensions
Capability.MULTIPLE_IMAGE_INPUT,
Capability.TEXT_OUTPUT,
Capability.IMAGE_OUTPUT,
Capability.WEB_SEARCH,
Capability.CHAT_COMPLETION_API,

View File

@ -5,6 +5,70 @@ namespace AIStudio.Settings;
public static partial class ProviderExtensions
{
/// <summary>
/// The longest model ID we normalize without going to the heap.
/// </summary>
private const int MAX_STACK_ALLOCATED_MODEL_ID_LENGTH = 256;
/// <summary>
/// Brings a model ID into the form the capability rules are written in.
/// </summary>
/// <remarks>
/// Every provider names the same model differently, and the difference is rarely in the words:
/// it is in what sits between them. Ollama separates the variant with a colon
/// ("qwen3.8:27b-mlx"), Blablador answers with a whole sentence ("10 - Muse Glimmer 30b - the
/// newest META model"), Fireworks puts a path in front
/// ("accounts/fireworks/models/llama-v3p1-405b-instruct"), and the hubs use hyphens. Without
/// this, every rule would have to spell out each of those writings, which is what the Llama
/// block used to do with four variants of one check.
///
/// The dots stay. They carry the version boundary: llama3 and llama3.1 are different models,
/// and only the latter calls functions. Dropping them would merge the two.
///
/// The patterns in the rules are written in this normalized form already, which is why they
/// use lowercase and hyphens throughout.
/// </remarks>
/// <param name="modelId">The model ID as the provider reports it.</param>
/// <returns>The model ID in lowercase, with every separator written as a single hyphen.</returns>
private static string NormalizeModelId(string modelId)
{
if (string.IsNullOrWhiteSpace(modelId))
return string.Empty;
//
// Normalizing never makes a name longer, so the original length is always enough room.
// Model IDs are short, which is why the buffer lives on the stack: the longest ones we
// know of are the descriptive names Blablador answers with, at around 75 characters. A
// provider reporting something longer still gets a correct answer, just from the heap.
//
Span<char> normalized = modelId.Length <= MAX_STACK_ALLOCATED_MODEL_ID_LENGTH
? stackalloc char[modelId.Length]
: new char[modelId.Length];
var length = 0;
foreach (var character in modelId)
{
if (char.IsAsciiLetterOrDigit(character) || character is '.')
{
normalized[length++] = char.ToLowerInvariant(character);
continue;
}
// Anything else separates two parts of the name. A leading separator, and a repeated
// one, say nothing and would only get in the way of the patterns:
if (length is 0 || normalized[length - 1] is '-')
continue;
normalized[length++] = '-';
}
// A trailing separator carries no meaning either:
if (length > 0 && normalized[length - 1] is '-')
length--;
return new string(normalized[..length]);
}
/// <summary>
/// Get the capabilities of the model used by the configured provider.
/// </summary>

View File

@ -7,7 +7,7 @@
- Added tools to assistant plugins and direct-chat launchers. Plugin authors name them in the new `ToolIds` field, either as the tools an assistant runs with or as the tools a launcher preselects for the chat it opens; the example assistant plugin shows both. Which tools an assistant asks for is part of what you get to see before you enable it: its security card names them, and the security audit takes them into account.
- Added tools to the Assistant Builder. For a direct-chat launcher you pick them yourself, alongside the workspace, provider, and data sources. For an assistant, the AI chooses from the tools installed here and says so in the draft, so you see the decision before the assistant is written.
- Added organization-wide management for tools. Among other options, IT departments can switch tools off entirely, disable individual ones, or define the provider trust a tool requires. You do not have to write any of it by hand: set a tool up in the app, then export its configuration as ready-made Lua code for your plugin, with encrypted API keys if you want them.
- Added tool calling to the abilities you can state yourself in the expert provider settings. When you use a model AI Studio does not recognize as tool-capable, you can now declare that it is, the same way you already could for image input or reasoning.
- Added tool calling to the abilities you can state yourself in the expert provider settings. When you use a model AI Studio does not recognize as tool-capable, you can now declare that it is, the same way you already could for image input or reasoning. It works in the other direction as well: a model AI Studio has never heard of is offered tools, because most models can use them by now. Should one turn out not to be able to, AI Studio says so in plain words and points you to the same setting to switch the ability off again, instead of only passing the provider's error on.
- Added local RAG as a beta feature, so the AI can answer from your own documents. You point AI Studio at a folder or at a single file, and it prepares those documents in the background so their contents can be found again later. Ask a question with such a data source selected, and AI Studio looks for the passages that fit your question and hands only those to the model, along with where each one came from. We will keep developing it together with the people who use it: to try it, open the app settings, allow preview features down to beta, and then enable the RAG feature. Many thanks to Paul Koudelka (`PaulKoudelka`) for around ten months of work on the concept and the implementation.
- Added the setup for local data sources. You pick an embedding provider, and AI Studio asks for your confirmation before any document goes to a cloud service. It keeps up with your files as they change, shows the progress on a page of its own, and checks every document for hidden instructions before indexing it. Documents without readable text, such as scanned pages, are remembered as such, so AI Studio does not work through them again after every start — it comes back to them once they change.
- Added support for several drop areas on the same page. More complex assistants can now receive files or folders by drag and drop at more than one place.
@ -15,6 +15,7 @@
- Added ways to load text from a file and drop zones for them, throughout the assistants and dialogs. We went through them one by one, so many fields that used to accept typed text only now take the content of a file as well.
- Improved loading web content in the assistants: it now uses the same reader as the Read Web Page tool, which extracts the main content of a page more reliably and skips navigation and boilerplate. Pages from your own network, including local servers, keep working as before. When a page cannot be read, AI Studio now says why instead of leaving the field empty.
- Changed how provider trust and provider confidence work together. Marking a provider as trustworthy in a configuration no longer also satisfies a required confidence level: one says who runs the provider, the other how confidential it is. Organizations raise a provider's level in their own confidence scheme instead. This applies beyond local data sources, for example, when a model reads a page from your intranet.
- Fixed which abilities AI Studio assumes a model has. Model names are now read the way each provider writes them, so models from self-hosted and research services are recognized instead of being treated as plain text models, and a model resold under a plain name gets the abilities it really has. Many model families were checked against their maker's documentation and corrected: some gained image input, reasoning, or tool calling, others lost an ability they never had. Image and video generation models no longer show up among the chat models.
- Fixed a dropped file being processed several times, e.g., after the computer woke up from sleep.
- Fixed the Visual Briefing Assistant (in preview) not scrolling, which put everything below the window edge out of reach and made the assistant unusable. The briefing preview is now shown at its intended size inside its frame, and switching between the desktop, tablet, and mobile view changes its width as it should.
- Upgraded the Visual Briefing Assistant (in preview) from the prototype to the beta state. The assistant is now completely implemented and is undergoing a deeper testing phase in preparation for release. To try it, open the app settings, allow preview features down to beta, and then enable the Visual Briefing Assistant there.