From 2da420e549185258a42e9cc7d8ea046332b68d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= <20603780+peerschuett@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:44:50 +0200 Subject: [PATCH] Removed Anthropic Provider support, because it differs too much from the Chat Completion / Responses API version --- .../Assistants/I18N/allTexts.lua | 12 +- .../Components/ToolSelection.razor | 4 +- .../Components/ToolSelection.razor.cs | 14 +- .../Provider/Anthropic/AnthropicToolModels.cs | 74 ----- .../Provider/Anthropic/ProviderAnthropic.cs | 274 +----------------- 5 files changed, 23 insertions(+), 355 deletions(-) delete mode 100644 app/MindWork AI Studio/Provider/Anthropic/AnthropicToolModels.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 26264199..42845fe1 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -3172,6 +3172,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3364063757"] = "The selec -- Close UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3448155331"] = "Close" +-- Tool calling for this provider is not implemented yet. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3776963202"] = "Tool calling for this provider is not implemented yet." + -- No tools are available in this context. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3904490680"] = "No tools are available in this context." @@ -6727,9 +6730,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T3948127789"] = "Suggestion" -- Your stage directions UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "Your stage directions" --- The tool calling request failed with status code {0}. See the logs for details. -UI_TEXT_CONTENT["AISTUDIO::PROVIDER::ANTHROPIC::PROVIDERANTHROPIC::T3117779001"] = "The tool calling request failed with status code {0}. See the logs for details." - -- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}' UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'" @@ -8017,9 +8017,6 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS: -- Maximum Content Characters UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2801581200"] = "Maximum Content Characters" --- 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.com. 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::T2866833707"] = "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.com. 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." - -- Optional HTTP timeout for loading a web page in seconds. UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2941521561"] = "Optional HTTP timeout for loading a web page in seconds." @@ -8038,6 +8035,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS: -- 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 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::T854695329"] = "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." + -- Maximum Results UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1273024715"] = "Maximum Results" diff --git a/app/MindWork AI Studio/Components/ToolSelection.razor b/app/MindWork AI Studio/Components/ToolSelection.razor index 23613c26..ef547703 100644 --- a/app/MindWork AI Studio/Components/ToolSelection.razor +++ b/app/MindWork AI Studio/Components/ToolSelection.razor @@ -3,7 +3,7 @@ @inherits MSGComponentBase
- + @@ -20,7 +20,7 @@ @if (!this.SupportsTools) { - @T("The selected provider or model does not support tool calling.") + @this.UnsupportedToolsMessage } else if (this.Disabled) { diff --git a/app/MindWork AI Studio/Components/ToolSelection.razor.cs b/app/MindWork AI Studio/Components/ToolSelection.razor.cs index 0f81eb0c..27f4ced5 100644 --- a/app/MindWork AI Studio/Components/ToolSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ToolSelection.razor.cs @@ -51,9 +51,21 @@ public partial class ToolSelection : MSGComponentBase private bool SupportsTools => this.LLMProvider != AIStudio.Settings.Provider.NONE && - this.LLMProvider.GetModelCapabilities().Contains(Capability.CHAT_COMPLETION_API) && + (this.LLMProvider.GetModelCapabilities().Contains(Capability.CHAT_COMPLETION_API) || + this.LLMProvider.GetModelCapabilities().Contains(Capability.RESPONSES_API)) && this.LLMProvider.GetModelCapabilities().Contains(Capability.FUNCTION_CALLING); + private bool IsAnthropicProvider => this.LLMProvider != AIStudio.Settings.Provider.NONE && + this.LLMProvider.UsedLLMProvider is LLMProviders.ANTHROPIC; + + private string ToolButtonTooltip => this.SupportsTools + ? this.T("Select tools") + : this.UnsupportedToolsMessage; + + private string UnsupportedToolsMessage => this.IsAnthropicProvider + ? this.T("Tool calling for this provider is not implemented yet.") + : this.T("The selected model does not support tool calling."); + private ConfidenceLevel ProviderConfidence => this.LLMProvider == AIStudio.Settings.Provider.NONE ? ConfidenceLevel.NONE : this.LLMProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level; diff --git a/app/MindWork AI Studio/Provider/Anthropic/AnthropicToolModels.cs b/app/MindWork AI Studio/Provider/Anthropic/AnthropicToolModels.cs deleted file mode 100644 index c026b41a..00000000 --- a/app/MindWork AI Studio/Provider/Anthropic/AnthropicToolModels.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Text.Json; - -namespace AIStudio.Provider.Anthropic; - -public sealed record AnthropicTool -{ - public string Name { get; init; } = string.Empty; - - public string Description { get; init; } = string.Empty; - - public bool Strict { get; init; } - - public JsonElement InputSchema { get; init; } -} - -public sealed record AnthropicMessage(IList Content, string Role) : IMessage>; - -public sealed record AnthropicToolResultMessage(IList Content, string Role = "user") : IMessage>; - -public sealed record AnthropicToolResultContent -{ - public string Type { get; init; } = "tool_result"; - - public string ToolUseId { get; init; } = string.Empty; - - public string Content { get; init; } = string.Empty; -} - -public sealed record AnthropicResponse -{ - public string StopReason { get; init; } = string.Empty; - - public IList Content { get; init; } = []; - - public IReadOnlyList GetToolUses() => this.Content - .Where(x => ReadString(x, "type").Equals("tool_use", StringComparison.Ordinal)) - .Select(x => new AnthropicToolUse - { - Id = ReadString(x, "id"), - Name = ReadString(x, "name"), - Input = x.TryGetProperty("input", out var input) ? input : default, - }) - .Where(x => !string.IsNullOrWhiteSpace(x.Id) && !string.IsNullOrWhiteSpace(x.Name)) - .ToList(); - - public string GetTextOutput() => string.Concat(this.Content - .Where(x => ReadString(x, "type").Equals("text", StringComparison.Ordinal)) - .Select(x => ReadString(x, "text"))); - - public bool HasFinalStopReason() => this.StopReason is $"" or "end_turn" or "stop_sequence"; - - private static string ReadString(JsonElement item, string propertyName) - { - if (item.ValueKind is not JsonValueKind.Object || - !item.TryGetProperty(propertyName, out var property) || - property.ValueKind is not JsonValueKind.String) - return string.Empty; - - return property.GetString() ?? string.Empty; - } -} - -public sealed record AnthropicToolUse -{ - public string Id { get; init; } = string.Empty; - - public string Name { get; init; } = string.Empty; - - public JsonElement Input { get; init; } - - public string Arguments => this.Input.ValueKind is JsonValueKind.Undefined - ? "{}" - : this.Input.GetRawText(); -} diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index d68193df..b56903a2 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -1,16 +1,12 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; -using System.Text.Json.Nodes; using AIStudio.Chat; using AIStudio.Provider.OpenAI; using AIStudio.Settings; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Rust; -using AIStudio.Tools.ToolCallingSystem; - -using Microsoft.Extensions.DependencyInjection; namespace AIStudio.Provider.Anthropic; @@ -39,7 +35,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n yield break; // Parse the API parameters: - var apiParameters = this.ParseAdditionalApiParameters("system", "tools"); + var apiParameters = this.ParseAdditionalApiParameters("system"); var maxTokens = 4_096; if (TryPopIntParameter(apiParameters, "max_tokens", out var parsedMaxTokens)) maxTokens = parsedMaxTokens; @@ -77,40 +73,6 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n } } ); - - var toolRegistry = Program.SERVICE_PROVIDER.GetService(); - var toolExecutor = Program.SERVICE_PROVIDER.GetService(); - var currentAssistantContent = chatThread.Blocks.LastOrDefault(x => x.Role is ChatRole.AI)?.Content as ContentText; - currentAssistantContent?.ToolInvocations.Clear(); - var providerConfidence = this.Provider.GetConfidence(settingsManager).Level; - IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools = toolRegistry is null - ? [] - : await toolRegistry.GetRunnableToolsAsync( - chatThread.RuntimeComponent, - chatThread.RuntimeSelectedToolIds, - this.Provider.GetModelCapabilities(chatModel), - providerConfidence, - settingsManager.IsToolSelectionVisible(chatThread.RuntimeComponent)); - - if (toolExecutor is not null && runnableTools.Count > 0) - { - var systemPrompt = chatThread.PrepareSystemPrompt(settingsManager, runnableTools.Select(x => x.Definition)); - await foreach (var content in this.StreamWithLocalTools( - chatModel, - messages, - systemPrompt, - maxTokens, - apiParameters, - runnableTools, - toolExecutor, - currentAssistantContent, - requestedSecret, - providerConfidence, - token)) - yield return content; - - yield break; - } // Prepare the Anthropic HTTP chat request: var chatRequest = JsonSerializer.Serialize(new ChatRequest @@ -148,169 +110,6 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n yield return content; } - private async IAsyncEnumerable StreamWithLocalTools( - Model chatModel, - IList baseMessages, - string systemPrompt, - int maxTokens, - IDictionary apiParameters, - IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools, - ToolExecutor toolExecutor, - ContentText? currentAssistantContent, - RequestedSecret requestedSecret, - ConfidenceLevel providerConfidence, - [EnumeratorCancellation] CancellationToken token) - { - var providerTools = runnableTools - .Select(x => (object)new AnthropicTool - { - Name = x.Definition.Function.Name, - Description = x.Definition.Function.Description, - Strict = x.Definition.Function.Strict, - InputSchema = NormalizeInputSchemaForAnthropic(x.Definition.Function.Parameters), - }) - .ToList(); - var internalMessages = new List(); - var toolCallCount = 0; - - while (true) - { - var requestDto = new ChatRequest - { - Model = chatModel.Id, - Messages = [..baseMessages, ..internalMessages], - MaxTokens = maxTokens, - Stream = false, - System = systemPrompt, - Tools = providerTools, - AdditionalApiParameters = apiParameters, - }; - var response = await this.ExecuteMessagesRequest(requestDto, requestedSecret, token); - if (response is null) - { - if (currentAssistantContent is not null) - { - currentAssistantContent.ToolRuntimeStatus = new(); - await currentAssistantContent.StreamingEvent(); - } - - yield break; - } - - var textOutput = response.GetTextOutput(); - var toolUses = response.GetToolUses(); - if (toolUses.Count > 0 && !string.IsNullOrWhiteSpace(textOutput)) - yield return new ContentStreamChunk(textOutput, []); - - if (toolUses.Count == 0) - { - if (currentAssistantContent is not null) - { - currentAssistantContent.ToolRuntimeStatus = new(); - await currentAssistantContent.StreamingEvent(); - } - - if (!string.IsNullOrWhiteSpace(textOutput)) - yield return new ContentStreamChunk(textOutput, []); - - if (!response.HasFinalStopReason()) - { - yield return new ContentStreamChunk($"The model stopped with reason '{response.StopReason}' before returning a final answer.", []); - yield break; - } - - else if (toolCallCount > 0) - yield return new ContentStreamChunk("The model completed the tool call but did not return a final answer.", []); - - yield break; - } - - if (currentAssistantContent is not null) - { - currentAssistantContent.ToolRuntimeStatus = new ToolRuntimeStatus - { - IsRunning = true, - ToolNames = toolUses - .Select(x => runnableTools.FirstOrDefault(tool => tool.Definition.Function.Name.Equals(x.Name, StringComparison.Ordinal)).Implementation?.GetDisplayName() ?? x.Name) - .ToList(), - }; - await currentAssistantContent.StreamingEvent(); - } - - internalMessages.Add(new AnthropicMessage(response.Content, "assistant")); - var toolResults = new List(); - foreach (var toolUse in toolUses) - { - toolCallCount++; - if (toolCallCount > ToolSelectionRules.MAX_TOOL_CALLS) - { - var limitMessage = ToolSelectionRules.GetMaxToolCallsLimitMessage(); - currentAssistantContent?.ToolInvocations.Add(new ToolInvocationTrace - { - Order = toolCallCount, - ToolId = toolUse.Name, - ToolName = toolUse.Name, - ToolCallId = toolUse.Id, - Status = ToolInvocationTraceStatus.BLOCKED, - StatusMessage = limitMessage, - Result = limitMessage, - }); - - if (currentAssistantContent is not null) - { - currentAssistantContent.ToolRuntimeStatus = new(); - await currentAssistantContent.StreamingEvent(); - } - - yield return new ContentStreamChunk(limitMessage, []); - yield break; - } - - var (toolContent, trace) = await toolExecutor.ExecuteAsync( - toolUse.Id, - toolUse.Name, - toolUse.Arguments, - runnableTools, - providerConfidence, - toolCallCount, - token); - - currentAssistantContent?.ToolInvocations.Add(trace); - toolResults.Add(new AnthropicToolResultContent - { - ToolUseId = toolUse.Id, - Content = toolContent, - }); - } - - internalMessages.Add(new AnthropicToolResultMessage(toolResults)); - - if (currentAssistantContent is not null) - await currentAssistantContent.StreamingEvent(); - } - } - - private async Task ExecuteMessagesRequest(ChatRequest requestDto, RequestedSecret requestedSecret, CancellationToken token) - { - using var request = new HttpRequestMessage(HttpMethod.Post, "messages"); - request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION)); - request.Headers.Add("anthropic-version", "2023-06-01"); - request.Content = new StringContent(JsonSerializer.Serialize(requestDto, JSON_SERIALIZER_OPTIONS), Encoding.UTF8, "application/json"); - - using var response = await this.HttpClient.SendAsync(request, token); - if (!response.IsSuccessStatusCode) - { - var responseBody = await response.Content.ReadAsStringAsync(token); - LOGGER.LogError("Tool calling Anthropic Messages API request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody); - await MessageBus.INSTANCE.SendError(new( - Icons.Material.Filled.Build, - string.Format(TB("The tool calling request failed with status code {0}. See the logs for details."), (int)response.StatusCode))); - return null; - } - - return await response.Content.ReadFromJsonAsync(JSON_SERIALIZER_OPTIONS, token); - } - #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously /// public override async IAsyncEnumerable StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default) @@ -368,9 +167,8 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n { return Task.FromResult(ModelLoadResult.FromModels([])); } - #endregion - + private Task LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null) { return this.LoadModelsResponse( @@ -393,72 +191,4 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n }, jsonSerializerOptions: JSON_SERIALIZER_OPTIONS); } - - private static JsonElement NormalizeInputSchemaForAnthropic(JsonElement schema) - { - JsonNode? root = JsonNode.Parse(schema.GetRawText()); - if (root is JsonObject rootObject) - NormalizeSchemaNode(rootObject); - - return JsonSerializer.SerializeToElement(root); - } - - private static void NormalizeSchemaNode(JsonObject schemaObject) - { - var allowsNull = DeclaresNullType(schemaObject["type"]); - if (allowsNull && schemaObject["enum"] is JsonArray enumArray) - { - for (var i = enumArray.Count - 1; i >= 0; i--) - { - if (enumArray[i]?.GetValueKind() is JsonValueKind.Null) - enumArray.RemoveAt(i); - } - } - - if (schemaObject["properties"] is JsonObject propertiesObject) - { - foreach (var property in propertiesObject) - { - if (property.Value is JsonObject childObject) - NormalizeSchemaNode(childObject); - } - } - - if (schemaObject["items"] is JsonObject itemsObject) - NormalizeSchemaNode(itemsObject); - - if (schemaObject["anyOf"] is JsonArray anyOfArray) - { - foreach (var entry in anyOfArray) - { - if (entry is JsonObject childObject) - NormalizeSchemaNode(childObject); - } - } - - if (schemaObject["oneOf"] is JsonArray oneOfArray) - { - foreach (var entry in oneOfArray) - { - if (entry is JsonObject childObject) - NormalizeSchemaNode(childObject); - } - } - - if (schemaObject["allOf"] is JsonArray allOfArray) - { - foreach (var entry in allOfArray) - { - if (entry is JsonObject childObject) - NormalizeSchemaNode(childObject); - } - } - } - - private static bool DeclaresNullType(JsonNode? typeNode) => typeNode switch - { - JsonValue value when value.TryGetValue(out var typeName) => typeName.Equals("null", StringComparison.Ordinal), - JsonArray array => array.Any(entry => entry is JsonValue value && value.TryGetValue(out var typeName) && typeName.Equals("null", StringComparison.Ordinal)), - _ => false, - }; }