Removed Anthropic Provider support, because it differs too much from the Chat Completion / Responses API version

This commit is contained in:
Peer Schütt 2026-06-11 09:44:50 +02:00
parent d0e1966e9e
commit 2da420e549
5 changed files with 23 additions and 355 deletions

View File

@ -3172,6 +3172,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3364063757"] = "The selec
-- Close -- Close
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3448155331"] = "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. -- No tools are available in this context.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::TOOLSELECTION::T3904490680"] = "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 -- Your stage directions
UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "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}' -- 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}'" 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 -- Maximum Content Characters
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::READWEBPAGETOOL::T2801581200"] = "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. -- 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." 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. -- 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." 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 -- Maximum Results
UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1273024715"] = "Maximum Results" UI_TEXT_CONTENT["AISTUDIO::TOOLS::TOOLCALLINGSYSTEM::TOOLCALLINGIMPLEMENTATIONS::SEARXNGWEBSEARCHTOOL::T1273024715"] = "Maximum Results"

View File

@ -3,7 +3,7 @@
@inherits MSGComponentBase @inherits MSGComponentBase
<div class="d-flex"> <div class="d-flex">
<MudTooltip Text="@T("Select tools")" Placement="Placement.Top"> <MudTooltip Text="@this.ToolButtonTooltip" Placement="Placement.Top">
<MudIconButton Icon="@Icons.Material.Filled.Build" Class="@this.PopoverButtonClasses" OnClick="@this.ToggleSelection"/> <MudIconButton Icon="@Icons.Material.Filled.Build" Class="@this.PopoverButtonClasses" OnClick="@this.ToggleSelection"/>
</MudTooltip> </MudTooltip>
@ -20,7 +20,7 @@
<MudCardContent Style="min-width: 28em; max-height: 60vh; max-width: 48vw; overflow: auto;"> <MudCardContent Style="min-width: 28em; max-height: 60vh; max-width: 48vw; overflow: auto;">
@if (!this.SupportsTools) @if (!this.SupportsTools)
{ {
<MudText Typo="Typo.body1">@T("The selected provider or model does not support tool calling.")</MudText> <MudText Typo="Typo.body1">@this.UnsupportedToolsMessage</MudText>
} }
else if (this.Disabled) else if (this.Disabled)
{ {

View File

@ -51,9 +51,21 @@ public partial class ToolSelection : MSGComponentBase
private bool SupportsTools => private bool SupportsTools =>
this.LLMProvider != AIStudio.Settings.Provider.NONE && 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); 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 private ConfidenceLevel ProviderConfidence => this.LLMProvider == AIStudio.Settings.Provider.NONE
? ConfidenceLevel.NONE ? ConfidenceLevel.NONE
: this.LLMProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level; : this.LLMProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level;

View File

@ -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<JsonElement> Content, string Role) : IMessage<IList<JsonElement>>;
public sealed record AnthropicToolResultMessage(IList<AnthropicToolResultContent> Content, string Role = "user") : IMessage<IList<AnthropicToolResultContent>>;
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<JsonElement> Content { get; init; } = [];
public IReadOnlyList<AnthropicToolUse> 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();
}

View File

@ -1,16 +1,12 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust; using AIStudio.Tools.Rust;
using AIStudio.Tools.ToolCallingSystem;
using Microsoft.Extensions.DependencyInjection;
namespace AIStudio.Provider.Anthropic; namespace AIStudio.Provider.Anthropic;
@ -39,7 +35,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
yield break; yield break;
// Parse the API parameters: // Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters("system", "tools"); var apiParameters = this.ParseAdditionalApiParameters("system");
var maxTokens = 4_096; var maxTokens = 4_096;
if (TryPopIntParameter(apiParameters, "max_tokens", out var parsedMaxTokens)) if (TryPopIntParameter(apiParameters, "max_tokens", out var parsedMaxTokens))
maxTokens = parsedMaxTokens; maxTokens = parsedMaxTokens;
@ -78,40 +74,6 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
} }
); );
var toolRegistry = Program.SERVICE_PROVIDER.GetService<ToolRegistry>();
var toolExecutor = Program.SERVICE_PROVIDER.GetService<ToolExecutor>();
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: // Prepare the Anthropic HTTP chat request:
var chatRequest = JsonSerializer.Serialize(new ChatRequest var chatRequest = JsonSerializer.Serialize(new ChatRequest
{ {
@ -148,169 +110,6 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
yield return content; yield return content;
} }
private async IAsyncEnumerable<ContentStreamChunk> StreamWithLocalTools(
Model chatModel,
IList<IMessageBase> baseMessages,
string systemPrompt,
int maxTokens,
IDictionary<string, object> 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<IMessageBase>();
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<AnthropicToolResultContent>();
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<AnthropicResponse?> 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<AnthropicResponse>(JSON_SERIALIZER_OPTIONS, token);
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc /> /// <inheritdoc />
public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default) public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
@ -368,7 +167,6 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
{ {
return Task.FromResult(ModelLoadResult.FromModels([])); return Task.FromResult(ModelLoadResult.FromModels([]));
} }
#endregion #endregion
private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null) private Task<ModelLoadResult> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
@ -393,72 +191,4 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
}, },
jsonSerializerOptions: JSON_SERIALIZER_OPTIONS); 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<string>(out var typeName) => typeName.Equals("null", StringComparison.Ordinal),
JsonArray array => array.Any(entry => entry is JsonValue value && value.TryGetValue<string>(out var typeName) && typeName.Equals("null", StringComparison.Ordinal)),
_ => false,
};
} }