From ead156ac4d1d7c06351cd56eb1e2a0bb41d70a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= <20603780+peerschuett@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:01:46 +0200 Subject: [PATCH] Set parallel_tool_calls to false in chat completions, because AI Studio calls all tools sequentially at the moment. Also fixed a bug that resulted in an error when using tools with Mistral API --- .../Assistants/I18N/allTexts.lua | 12 +++--- .../Chat/ChatThreadExtensions.cs | 9 +++- .../Plugins/configuration/plugin.lua | 9 ++-- .../AlibabaCloud/ProviderAlibabaCloud.cs | 1 - .../Provider/BaseProvider.cs | 11 ++--- .../Provider/DeepSeek/ProviderDeepSeek.cs | 1 - .../Provider/Fireworks/ProviderFireworks.cs | 1 - .../Provider/GWDG/ProviderGWDG.cs | 1 - .../Provider/Google/ProviderGoogle.cs | 1 - .../Provider/Groq/ProviderGroq.cs | 1 - .../Provider/Helmholtz/ProviderHelmholtz.cs | 1 - .../HuggingFace/ProviderHuggingFace.cs | 1 - .../Provider/Mistral/ProviderMistral.cs | 3 +- .../OpenAI/AssistantToolCallMessage.cs | 3 +- .../Provider/OpenAI/ChatCompletionChoice.cs | 4 +- .../Provider/OpenAI/ChatCompletionContent.cs | 41 +++++++++++++++++++ .../Provider/OpenAI/ChatCompletionDelta.cs | 16 +++++--- .../OpenAI/ChatCompletionResponseMessage.cs | 9 +++- .../Provider/OpenAI/ProviderOpenAI.cs | 5 +-- .../Provider/OpenRouter/ProviderOpenRouter.cs | 3 +- .../Provider/Perplexity/ProviderPerplexity.cs | 1 - .../Provider/SelfHosted/ProviderSelfHosted.cs | 1 - .../Provider/X/ProviderX.cs | 1 - .../DataSourceSecurityTrustExtensions.cs | 4 +- .../ReadWebPageTool.cs | 7 ++-- .../ToolCallingSystem/ToolExecutionModels.cs | 2 + .../Tools/ToolCallingSystem/ToolExecutor.cs | 9 ++-- .../ToolCallingSystem/ToolSelectionRules.cs | 4 +- .../Tools/Web/WebPageRetrievalService.cs | 8 ++-- 29 files changed, 111 insertions(+), 59 deletions(-) create mode 100644 app/MindWork AI Studio/Provider/OpenAI/ChatCompletionContent.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 6bb32e8a..9cf460bb 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -9241,12 +9241,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGAVAILABILITYEXTE -- Tool calling for this provider is not implemented yet. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGAVAILABILITYEXTENSIONS::T3776963202"] = "Tool calling for this provider is not implemented yet." --- (Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T1410249500"] = "(Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication." - -- Allowed private hosts must be host names only, without scheme or path. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2196457612"] = "Allowed private hosts must be host names only, without scheme or path." +-- The web page was not loaded because private or VPN web pages require a High-confidence provider or a provider trusted by your organization's configuration. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2563437007"] = "The web page was not loaded because private or VPN web pages require a High-confidence provider or a provider trusted by your organization's configuration." + -- Maximum Content Characters UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2801581200"] = "Maximum Content Characters" @@ -9265,15 +9265,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS: -- Load a web page and extract its readable content, links, and page details. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3715690061"] = "Load a web page and extract its readable content, links, and page details." --- The web page was not loaded because private or VPN web pages require a High-confidence provider. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T3856267430"] = "The web page was not loaded because private or VPN web pages require a High-confidence provider." - -- (Optional) HTTP timeout for loading a web page in seconds. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T4126164830"] = "(Optional) HTTP timeout for loading a web page in seconds." -- The setting '{0}' must be a positive integer. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T4199432074"] = "The setting '{0}' must be a positive integer." +-- (Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a High-confidence provider or a provider trusted by your organization's configuration. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T64466871"] = "(Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a High-confidence provider or a provider trusted by your organization's configuration. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication." + -- (Optional) Global truncation limit for extracted characters returned to the model. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T900659180"] = "(Optional) Global truncation limit for extracted characters returned to the model." diff --git a/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs b/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs index 256a2689..dd2f2941 100644 --- a/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs +++ b/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs @@ -35,7 +35,14 @@ public static class ChatThreadExtensions _ => ConfidenceLevel.UNKNOWN, }; - if (providerConfidence < chatThread.RequiredProviderConfidence) + var isTrustedByConfiguration = provider switch + { + IProvider p => p.IsTrustedByConfiguration(settingsManager), + AIStudio.Settings.Provider p => p.IsTrustedByConfiguration(settingsManager), + + _ => false, + }; + if (providerConfidence < chatThread.RequiredProviderConfidence && !isTrustedByConfiguration) return false; // The chat thread is available, but the data security is not specified. diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index e12f9f77..c154eadf 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -368,10 +368,10 @@ CONFIG["SETTINGS"] = {} -- Configure the minimum provider confidence level required for individual tools. -- Tool IDs include: web_search, read_web_page -- Allowed values are: NONE, UNTRUSTED, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH --- Defaults: web_search = MEDIUM, read_web_page = MEDIUM, but higher confidence is recommended +-- Defaults: web_search = VERY_LOW, read_web_page = VERY_LOW -- CONFIG["SETTINGS"]["DataTools.MinimumProviderConfidenceByToolId"] = { --- ["web_search"] = "MEDIUM", --- ["read_web_page"] = "MEDIUM" +-- ["web_search"] = "VERY_LOW", +-- ["read_web_page"] = "VERY_LOW" -- } -- Configure the Web Search tool. All values are strings. @@ -489,7 +489,8 @@ CONFIG["SETTINGS"] = {} -- Configure provider instances trusted by your organization for data-source security checks. -- These IDs may refer to LLM providers, embedding providers, or transcription providers -- defined in this configuration. Trusted providers are treated like self-hosted providers --- only for data-source security checks and related local data warnings. +-- only for data-source security checks and related local data warnings. Trusted LLM providers +-- can also use read_web_page for explicitly allowed private or VPN hosts. -- CONFIG["SETTINGS"]["DataSourceSecuritySettings.TrustedProviderIds"] = { -- "00000000-0000-0000-0000-000000000000", -- "00000000-0000-0000-0000-000000000001", diff --git a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs index 80e2b47d..0cad6c1c 100644 --- a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs +++ b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs @@ -45,7 +45,6 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 32d4d68e..366c0513 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -982,7 +982,7 @@ public abstract class BaseProvider : IProvider, ISecretId yield break; // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters(); + var apiParameters = this.ParseAdditionalApiParameters("parallel_tool_calls"); var toolRegistry = Program.SERVICE_PROVIDER.GetService(); var toolExecutor = Program.SERVICE_PROVIDER.GetService(); @@ -1054,10 +1054,7 @@ public abstract class BaseProvider : IProvider, ISecretId { Messages = [..requestDtoBase.Messages, ..internalMessages], Stream = false, - ParallelToolCalls = requestDtoBase.Tools is not null && - chatModel.Id.Contains("gpt-oss", StringComparison.InvariantCultureIgnoreCase) - ? false - : requestDtoBase.ParallelToolCalls, + ParallelToolCalls = requestDtoBase.Tools is null ? null : false, }; var response = await this.ExecuteChatCompletionRequest(requestDto, requestPath, requestedSecret, headersAction, token); var responseMessage = response?.Choices.FirstOrDefault()?.Message; @@ -1097,7 +1094,7 @@ public abstract class BaseProvider : IProvider, ISecretId internalMessages.Add(new AssistantToolCallMessage { - Content = responseMessage.Content, + Content = responseMessage.RawContent, ReasoningContent = responseMessage.ReasoningContent, ToolCalls = toolCalls, }); @@ -1121,7 +1118,7 @@ public abstract class BaseProvider : IProvider, ISecretId toolCall.Function.Name, toolCall.Function.Arguments, runnableTools, - this.Provider.GetConfidence(settingsManager).Level, + this, toolCallCount, token); toolResultCharacterCount += toolContent.Length; diff --git a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs index 4f8e8fb7..22ce388f 100644 --- a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs +++ b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs @@ -45,7 +45,6 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, ne Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index 907b8099..afdbae6b 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -46,7 +46,6 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, new Uri( // Right now, we only support streaming completions: Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, diff --git a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs index 17d64206..2a894b65 100644 --- a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs +++ b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs @@ -45,7 +45,6 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, new Uri("ht Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs index b9353e72..1966d511 100644 --- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs +++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs @@ -48,7 +48,6 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https // Right now, we only support streaming completions: Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, diff --git a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs index cbf80feb..d72dc909 100644 --- a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs +++ b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs @@ -49,7 +49,6 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, new Uri("https://a // Right now, we only support streaming completions: Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, diff --git a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs index de457975..789a4173 100644 --- a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs +++ b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs @@ -47,7 +47,6 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, n Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index 23570f96..5dd305e0 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -50,7 +50,6 @@ public sealed class ProviderHuggingFace : BaseProvider Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index a2c1fea2..03b94a53 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -52,7 +52,6 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U // Right now, we only support streaming completions: Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, @@ -141,4 +140,4 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, new U token, apiKeyProvisional); } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs b/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs index 693b40bd..cd404878 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/AssistantToolCallMessage.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; namespace AIStudio.Provider.OpenAI; @@ -6,7 +7,7 @@ public sealed record AssistantToolCallMessage : IMessageBase { public string Role { get; init; } = "assistant"; - public string? Content { get; init; } + public JsonElement? Content { get; init; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ReasoningContent { get; init; } diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionChoice.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionChoice.cs index 136cc367..22788bbe 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionChoice.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionChoice.cs @@ -7,7 +7,7 @@ namespace AIStudio.Provider.OpenAI; /// The delta text of the choice. public record ChatCompletionChoice(int Index, ChatCompletionDelta Delta) { - public ChatCompletionChoice() : this(0, new (string.Empty)) + public ChatCompletionChoice() : this(0, new()) { } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionContent.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionContent.cs new file mode 100644 index 00000000..84aabcf8 --- /dev/null +++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionContent.cs @@ -0,0 +1,41 @@ +using System.Text; +using System.Text.Json; + +namespace AIStudio.Provider.OpenAI; + +internal static class ChatCompletionContent +{ + public static string? GetText(JsonElement? content) + { + if (content is not { } value) + return null; + + if (value.ValueKind is JsonValueKind.String) + return value.GetString(); + + if (value.ValueKind is not JsonValueKind.Array) + return null; + + var text = new StringBuilder(); + foreach (var chunk in value.EnumerateArray()) + { + if (chunk.ValueKind is JsonValueKind.String) + { + text.Append(chunk.GetString()); + continue; + } + + if (chunk.ValueKind is not JsonValueKind.Object || + !chunk.TryGetProperty("type", out var type) || + type.ValueKind is not JsonValueKind.String || + !string.Equals(type.GetString(), "text", StringComparison.Ordinal) || + !chunk.TryGetProperty("text", out var textElement) || + textElement.ValueKind is not JsonValueKind.String) + continue; + + text.Append(textElement.GetString()); + } + + return text.Length == 0 ? null : text.ToString(); + } +} diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDelta.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDelta.cs index 6154cfbe..9f6f6cfc 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDelta.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDelta.cs @@ -1,12 +1,16 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + namespace AIStudio.Provider.OpenAI; /// /// The delta text of a choice. /// -/// The content of the delta text. -public record ChatCompletionDelta(string Content) +public sealed record ChatCompletionDelta { - public ChatCompletionDelta() : this(string.Empty) - { - } -} \ No newline at end of file + [JsonPropertyName("content")] + public JsonElement? RawContent { get; init; } + + [JsonIgnore] + public string Content => ChatCompletionContent.GetText(this.RawContent) ?? string.Empty; +} diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs index 205194e4..51fcdcce 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionResponseMessage.cs @@ -1,10 +1,17 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + namespace AIStudio.Provider.OpenAI; public sealed record ChatCompletionResponseMessage { public string Role { get; init; } = string.Empty; - public string? Content { get; init; } + [JsonPropertyName("content")] + public JsonElement? RawContent { get; init; } + + [JsonIgnore] + public string? Content => ChatCompletionContent.GetText(this.RawContent); public string? ReasoningContent { get; init; } diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index 1cba21d4..181f5a29 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -157,7 +157,6 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur Messages = [systemPrompt, ..messages], Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters, }; }, @@ -235,7 +234,6 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur toolExecutor, currentAssistantContent, requestedSecret, - providerConfidence, token)) yield return content; @@ -318,7 +316,6 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur ToolExecutor toolExecutor, ContentText? currentAssistantContent, RequestedSecret requestedSecret, - ConfidenceLevel providerConfidence, [EnumeratorCancellation] CancellationToken token) { var localProviderTools = runnableTools @@ -424,7 +421,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur functionCall.Name, functionCall.Arguments, runnableTools, - providerConfidence, + this, toolCallCount, token); toolResultCharacterCount += toolContent.Length; diff --git a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs index 7ed43dba..bd25c35e 100644 --- a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs +++ b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs @@ -50,7 +50,6 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER // Right now, we only support streaming completions: Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, @@ -152,4 +151,4 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER request.Headers.Add("X-Title", PROJECT_NAME); }); } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs index 5d10c083..d3a10841 100644 --- a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs +++ b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs @@ -53,7 +53,6 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY, Messages = [systemPrompt, ..messages], Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index 5b056fff..fb15026f 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -58,7 +58,6 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide // Right now, we only support streaming completions: Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, diff --git a/app/MindWork AI Studio/Provider/X/ProviderX.cs b/app/MindWork AI Studio/Provider/X/ProviderX.cs index 49a20797..e9bff88d 100644 --- a/app/MindWork AI Studio/Provider/X/ProviderX.cs +++ b/app/MindWork AI Studio/Provider/X/ProviderX.cs @@ -46,7 +46,6 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, new Uri("https:// // Right now, we only support streaming completions: Stream = true, Tools = tools, - ParallelToolCalls = tools is null ? null : true, AdditionalApiParameters = apiParameters }; }, diff --git a/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs b/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs index bff1f898..360bebe3 100644 --- a/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs @@ -42,6 +42,8 @@ public static class DataSourceSecurityTrustExtensions public static bool IsTrustedByConfiguration(this TranscriptionProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager); + public static bool IsTrustedByConfiguration(this IProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.ConfiguredProviderId, settingsManager); + private static bool IsTrustedProviderId(string providerId, SettingsManager settingsManager) { if (string.IsNullOrWhiteSpace(providerId)) @@ -49,4 +51,4 @@ public static class DataSourceSecurityTrustExtensions return settingsManager.ConfigurationData.DataSourceSecurity.TrustedProviderIds.Any(id => string.Equals(id, providerId, StringComparison.OrdinalIgnoreCase)); } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/ReadWebPageTool.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/ReadWebPageTool.cs index d0f286f5..1e0e26e8 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/ReadWebPageTool.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolCallingImplementations/ReadWebPageTool.cs @@ -39,7 +39,7 @@ public sealed class ReadWebPageTool(WebPageRetrievalService webPageRetrievalServ { "timeoutSeconds" => TB("(Optional) HTTP timeout for loading a web page in seconds."), "maxContentCharacters" => TB("(Optional) Global truncation limit for extracted characters returned to the model."), - ALLOWED_PRIVATE_HOSTS_SETTING => TB("(Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a high-confidence provider. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication."), + ALLOWED_PRIVATE_HOSTS_SETTING => TB("(Optional) Host allowlist for private or VPN web pages. For security reasons, private or VPN web pages aren't allowed to be read by default. Separate host patterns with commas, such as example.de, *.example.de. Allowed private hosts require a High-confidence provider or a provider trusted by your organization's configuration. For allowed internal hosts, AI Studio also tries the operating system's default sign-in automatically when the server responds with integrated authentication."), _ => TB(fieldDefinition.Description), }; @@ -113,6 +113,7 @@ public sealed class ReadWebPageTool(WebPageRetrievalService webPageRetrievalServ { TimeoutSeconds = timeoutSeconds, ProviderConfidence = context.ProviderConfidence, + ProviderIsTrustedByConfiguration = context.ProviderIsTrustedByConfiguration, UseOsSso = true, IsPrivateHostAllowed = host => IsAllowedPrivateHost(host, allowedPrivateHosts), OnPrivateHostProviderBlockAsync = this.ReportPrivateHostProviderBlockAsync, @@ -239,13 +240,13 @@ public sealed class ReadWebPageTool(WebPageRetrievalService webPageRetrievalServ private async Task ReportPrivateHostProviderBlockAsync(Uri url, ConfidenceLevel providerConfidence) { logger.LogWarning( - "Blocked read_web_page access to allowed private host '{Host}' because provider confidence '{ProviderConfidence}' is below HIGH.", + "Blocked read_web_page access to allowed private host '{Host}' because provider confidence '{ProviderConfidence}' is below HIGH and the provider is not trusted by configuration.", url.Host, providerConfidence); await MessageBus.INSTANCE.SendError(new DataErrorMessage( Icons.Material.Filled.Security, - TB("The web page was not loaded because private or VPN web pages require a High-confidence provider."))); + TB("The web page was not loaded because private or VPN web pages require a High-confidence provider or a provider trusted by your organization's configuration."))); } private static bool IsAllowedPrivateHost(string host, IReadOnlyList allowedPrivateHosts) diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs index 8adb91a1..3383e7e0 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutionModels.cs @@ -18,6 +18,8 @@ public sealed class ToolExecutionContext public required IReadOnlyDictionary SettingsValues { get; init; } public ConfidenceLevel ProviderConfidence { get; init; } = ConfidenceLevel.UNKNOWN; + + public bool ProviderIsTrustedByConfiguration { get; init; } } public sealed class ToolExecutionResult diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutor.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutor.cs index 272c8980..752da7df 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutor.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutor.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Text.Json; using AIStudio.Provider; +using AIStudio.Settings; using Microsoft.Extensions.DependencyInjection; @@ -14,7 +15,7 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge string toolName, string argumentsJson, IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools, - ConfidenceLevel providerConfidence, + IProvider provider, int order, CancellationToken token = default) { @@ -57,13 +58,15 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge { using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson); var settingsValues = await toolSettingsService.GetSettingsAsync(definition); + var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService(); var result = await implementation.ExecuteAsync(document.RootElement, new ToolExecutionContext { Definition = definition, ToolCallId = toolCallId, - SettingsManager = Program.SERVICE_PROVIDER.GetRequiredService(), + SettingsManager = settingsManager, SettingsValues = settingsValues, - ProviderConfidence = providerConfidence, + ProviderConfidence = provider.Provider.GetConfidence(settingsManager).Level, + ProviderIsTrustedByConfiguration = provider.IsTrustedByConfiguration(settingsManager), }, token); logger.LogInformation("Completed tool execution. ToolName={ToolName}, ToolCallId={ToolCallId}, DurationMs={DurationMs}, Status={Status}", toolName, toolCallId, stopwatch.ElapsedMilliseconds, ToolInvocationTraceStatus.SUCCESS); diff --git a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs index ae83b7aa..68487adf 100644 --- a/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs +++ b/app/MindWork AI Studio/Tools/ToolCallingSystem/ToolSelectionRules.cs @@ -17,8 +17,8 @@ public static class ToolSelectionRules public static ConfidenceLevel GetDefaultMinimumProviderConfidence(string toolId) => toolId switch { - WEB_SEARCH_TOOL_ID => ConfidenceLevel.MEDIUM, - READ_WEB_PAGE_TOOL_ID => ConfidenceLevel.MEDIUM, + WEB_SEARCH_TOOL_ID => ConfidenceLevel.VERY_LOW, + READ_WEB_PAGE_TOOL_ID => ConfidenceLevel.VERY_LOW, _ => ConfidenceLevel.NONE, }; diff --git a/app/MindWork AI Studio/Tools/Web/WebPageRetrievalService.cs b/app/MindWork AI Studio/Tools/Web/WebPageRetrievalService.cs index 7582914b..7ea8d6cf 100644 --- a/app/MindWork AI Studio/Tools/Web/WebPageRetrievalService.cs +++ b/app/MindWork AI Studio/Tools/Web/WebPageRetrievalService.cs @@ -119,13 +119,13 @@ public sealed class WebPageRetrievalService(HTMLParser htmlParser) "Private or local-network web page URLs are not supported unless their host is explicitly allowed.", WebPageAccessBlockReason.PRIVATE_HOST_NOT_ALLOWED); - if (options.ProviderConfidence >= ConfidenceLevel.HIGH) + if (options.ProviderConfidence >= ConfidenceLevel.HIGH || options.ProviderIsTrustedByConfiguration) return addresses; if (options.OnPrivateHostProviderBlockAsync is not null) await options.OnPrivateHostProviderBlockAsync(url, options.ProviderConfidence); throw new WebPageAccessBlockedException( - "This private or VPN web page requires a High-confidence provider.", + "This private or VPN web page requires a High-confidence provider or a provider trusted by configuration.", WebPageAccessBlockReason.INSUFFICIENT_PROVIDER_CONFIDENCE); } @@ -152,7 +152,7 @@ public sealed class WebPageRetrievalService(HTMLParser htmlParser) IReadOnlyList addresses, WebPageRetrievalOptions options) => options.UseOsSso && - options.ProviderConfidence >= ConfidenceLevel.HIGH && + (options.ProviderConfidence >= ConfidenceLevel.HIGH || options.ProviderIsTrustedByConfiguration) && originalUrl.Scheme.Equals(candidateUrl.Scheme, StringComparison.OrdinalIgnoreCase) && originalUrl.Host.Equals(candidateUrl.Host, StringComparison.OrdinalIgnoreCase) && originalUrl.Port == candidateUrl.Port && @@ -240,6 +240,8 @@ public sealed class WebPageRetrievalOptions public ConfidenceLevel ProviderConfidence { get; init; } = ConfidenceLevel.NONE; + public bool ProviderIsTrustedByConfiguration { get; init; } + public bool UseOsSso { get; init; } public Func? IsPrivateHostAllowed { get; init; }