Skip to content
3 changes: 3 additions & 0 deletions app/MindWork AI Studio/Assistants/I18N/allTexts.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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."

Expand Down
2 changes: 1 addition & 1 deletion app/MindWork AI Studio/Plugins/configuration/plugin.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"] = {
Expand Down
75 changes: 75 additions & 0 deletions app/MindWork AI Studio/Provider/BaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ protected async Task<ModelLoadResult> LoadModelsResponse<TResponse>(SecretStoreT
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,
Expand Down Expand Up @@ -340,6 +341,9 @@ private static bool IsContextLengthFailure(string responseBody) =>

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;

Expand All @@ -351,9 +355,80 @@ protected virtual ProviderRequestFailureReason ClassifyProviderRequestFailure(st
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))
Expand Down
13 changes: 11 additions & 2 deletions app/MindWork AI Studio/Provider/ModelKindExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -107,7 +116,7 @@ public static ModelKind DetermineKind(this Model model)
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))
Expand Down
10 changes: 10 additions & 0 deletions app/MindWork AI Studio/Provider/ProviderRequestFailureReason.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
95 changes: 85 additions & 10 deletions app/MindWork AI Studio/Settings/ProviderExtensions.Alibaba.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,41 @@ 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,
Capability.AUDIO_INPUT, Capability.SPEECH_INPUT,
Capability.VIDEO_INPUT,

Capability.TEXT_OUTPUT, Capability.SPEECH_OUTPUT,

Capability.CHAT_COMPLETION_API,
];

}

// Check for Qwen 3.5:
if(modelName.StartsWith("qwen3.5"))
return
Expand All @@ -47,6 +64,44 @@ private static List<Capability> GetModelCapabilitiesAlibaba(Model model)
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"))
{
Expand Down Expand Up @@ -84,15 +139,28 @@ private static List<Capability> GetModelCapabilitiesAlibaba(Model model)
];
}

// 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"))
Expand All @@ -106,15 +174,22 @@ private static List<Capability> GetModelCapabilitiesAlibaba(Model model)
];
}

// 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,
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private static List<Capability> GetModelCapabilitiesGateway(Model model)
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
{
Expand Down Expand Up @@ -69,6 +69,10 @@ private static List<Capability> GetModelCapabilitiesGateway(Model model)
/// 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)
{
Expand Down
Loading