mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-14 21:03:37 +00:00
Fixed model capabilities and how model names are matched (#958)
This commit is contained in:
parent
e64c51bd57
commit
2834529753
@ -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."
|
||||
|
||||
|
||||
@ -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"] = {
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
@ -20,10 +36,11 @@ public static partial class ProviderExtensions
|
||||
Capability.VIDEO_INPUT,
|
||||
|
||||
Capability.TEXT_OUTPUT, Capability.SPEECH_OUTPUT,
|
||||
|
||||
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
// Check for Qwen 3.5:
|
||||
if(modelName.StartsWith("qwen3.5"))
|
||||
return
|
||||
@ -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,15 +139,28 @@ 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,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
}
|
||||
|
||||
// Check for Qwen 3:
|
||||
if(modelName.StartsWith("qwen3"))
|
||||
@ -106,15 +174,22 @@ public static partial class ProviderExtensions
|
||||
];
|
||||
}
|
||||
|
||||
// QwQ models:
|
||||
//
|
||||
// QwQ models. What Model Studio serves under this name is qwq-plus, a commercial
|
||||
// thinking-only model built on Qwen2.5. It is not the same model as the open-weight
|
||||
// QwQ-32B, which the rules for open source models cover; the two only share a family
|
||||
// name. Neither of them appears in Alibaba's list of models which call functions, and
|
||||
// the model card of the open weights does not mention tools at all, which is why this
|
||||
// states no such ability. Anybody who knows better can turn it on in the expert settings.
|
||||
//
|
||||
if (modelName.StartsWith("qwq"))
|
||||
{
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT,
|
||||
Capability.TEXT_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
Capability.ALWAYS_REASONING, Capability.FUNCTION_CALLING,
|
||||
|
||||
Capability.ALWAYS_REASONING,
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
}
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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,17 +114,17 @@ 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)
|
||||
return
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
[
|
||||
@ -116,8 +108,9 @@ public static partial class ProviderExtensions
|
||||
[
|
||||
Capability.TEXT_INPUT, Capability.MULTIPLE_IMAGE_INPUT,
|
||||
Capability.TEXT_OUTPUT,
|
||||
|
||||
|
||||
Capability.FUNCTION_CALLING, Capability.ALWAYS_REASONING,
|
||||
Capability.WEB_SEARCH,
|
||||
Capability.RESPONSES_API,
|
||||
];
|
||||
|
||||
@ -132,12 +125,19 @@ 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,
|
||||
Capability.RESPONSES_API, Capability.CHAT_COMPLETION_API,
|
||||
@ -147,8 +147,8 @@ 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,
|
||||
Capability.RESPONSES_API, Capability.CHAT_COMPLETION_API,
|
||||
@ -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,
|
||||
@ -197,7 +197,22 @@ public static partial class ProviderExtensions
|
||||
Capability.WEB_SEARCH,
|
||||
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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -6,31 +6,34 @@ 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
|
||||
[
|
||||
Capability.TEXT_INPUT,
|
||||
Capability.MULTIPLE_IMAGE_INPUT,
|
||||
|
||||
|
||||
Capability.TEXT_OUTPUT,
|
||||
Capability.IMAGE_OUTPUT,
|
||||
|
||||
|
||||
Capability.ALWAYS_REASONING,
|
||||
Capability.WEB_SEARCH,
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
|
||||
|
||||
return
|
||||
[
|
||||
Capability.TEXT_INPUT,
|
||||
Capability.MULTIPLE_IMAGE_INPUT,
|
||||
|
||||
|
||||
Capability.TEXT_OUTPUT,
|
||||
Capability.IMAGE_OUTPUT,
|
||||
|
||||
|
||||
Capability.WEB_SEARCH,
|
||||
Capability.CHAT_COMPLETION_API,
|
||||
];
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user