2024-06-30 18:56:08 +00:00
using System.Runtime.CompilerServices ;
using System.Text ;
using System.Text.Json ;
2026-06-05 08:32:53 +00:00
using System.Text.Json.Nodes ;
2024-06-30 18:56:08 +00:00
using AIStudio.Chat ;
using AIStudio.Provider.OpenAI ;
2025-01-02 13:50:54 +00:00
using AIStudio.Settings ;
2026-06-05 08:32:53 +00:00
using AIStudio.Tools.PluginSystem ;
2026-06-03 21:22:38 +00:00
using AIStudio.Tools.Rust ;
using AIStudio.Tools.ToolCallingSystem ;
using Microsoft.Extensions.DependencyInjection ;
2024-06-30 18:56:08 +00:00
namespace AIStudio.Provider.Anthropic ;
2026-05-31 16:46:54 +00:00
public sealed class ProviderAnthropic ( ) : BaseProvider ( LLMProviders . ANTHROPIC , new Uri ( "https://api.anthropic.com/v1/" ) , ExternalHttpTrustPolicy . SYSTEM_TRUST_ONLY , LOGGER )
2024-06-30 18:56:08 +00:00
{
2025-09-03 19:25:17 +00:00
private static readonly ILogger < ProviderAnthropic > LOGGER = Program . LOGGER_FACTORY . CreateLogger < ProviderAnthropic > ( ) ;
2026-06-03 21:22:38 +00:00
private static string TB ( string fallbackEN ) = > I18N . I . T ( fallbackEN , typeof ( ProviderAnthropic ) . Namespace , nameof ( ProviderAnthropic ) ) ;
2025-09-03 19:25:17 +00:00
2024-06-30 18:56:08 +00:00
#region Implementation of IProvider
2026-04-16 09:24:22 +00:00
/// <inheritdoc />
2024-12-03 14:24:40 +00:00
public override string Id = > LLMProviders . ANTHROPIC . ToName ( ) ;
2024-06-30 18:56:08 +00:00
2026-04-16 09:24:22 +00:00
/// <inheritdoc />
2024-12-03 14:24:40 +00:00
public override string InstanceName { get ; set ; } = "Anthropic" ;
2024-06-30 18:56:08 +00:00
2026-04-16 09:24:22 +00:00
/// <inheritdoc />
public override bool HasModelLoadingCapability = > true ;
2024-06-30 18:56:08 +00:00
/// <inheritdoc />
2025-08-31 12:27:35 +00:00
public override async IAsyncEnumerable < ContentStreamChunk > StreamChatCompletion ( Model chatModel , ChatThread chatThread , SettingsManager settingsManager , [ EnumeratorCancellation ] CancellationToken token = default )
2024-06-30 18:56:08 +00:00
{
// Get the API key:
2026-01-11 15:02:28 +00:00
var requestedSecret = await RUST_SERVICE . GetAPIKey ( this , SecretStoreType . LLM_PROVIDER ) ;
2024-06-30 18:56:08 +00:00
if ( ! requestedSecret . Success )
yield break ;
2025-11-13 17:13:16 +00:00
// Parse the API parameters:
2026-06-03 21:22:38 +00:00
var apiParameters = this . ParseAdditionalApiParameters ( "system" , "tools" ) ;
2026-03-12 11:11:54 +00:00
var maxTokens = 4_096 ;
if ( TryPopIntParameter ( apiParameters , "max_tokens" , out var parsedMaxTokens ) )
maxTokens = parsedMaxTokens ;
2024-06-30 18:56:08 +00:00
2025-12-10 12:48:13 +00:00
// Build the list of messages:
2025-12-30 17:30:32 +00:00
var messages = await chatThread . Blocks . BuildMessagesAsync (
this . Provider , chatModel ,
// Anthropic-specific role mapping:
role = > role switch
2025-12-10 12:48:13 +00:00
{
ChatRole . USER = > "user" ,
ChatRole . AI = > "assistant" ,
ChatRole . AGENT = > "assistant" ,
_ = > "user" ,
} ,
2025-12-30 17:30:32 +00:00
// Anthropic uses the standard text sub-content:
text = > new SubContentText
{
Text = text ,
} ,
// Anthropic-specific image sub-content:
async attachment = > new SubContentImage
2025-12-10 12:48:13 +00:00
{
2025-12-30 17:30:32 +00:00
Source = new SubContentBase64Image
{
Data = await attachment . TryAsBase64 ( token : token ) is ( true , var base64Content )
? base64Content
: string . Empty ,
MediaType = attachment . DetermineMimeType ( ) ,
}
2025-12-10 12:48:13 +00:00
}
2025-12-30 17:30:32 +00:00
) ;
2025-12-10 12:48:13 +00:00
2026-06-03 21:22:38 +00:00
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 )
{
2026-06-10 09:29:23 +00:00
var systemPrompt = chatThread . PrepareSystemPrompt ( settingsManager , runnableTools . Select ( x = > x . Definition ) ) ;
2026-06-03 21:22:38 +00:00
await foreach ( var content in this . StreamWithLocalTools (
chatModel ,
messages ,
2026-06-10 09:29:23 +00:00
systemPrompt ,
2026-06-03 21:22:38 +00:00
maxTokens ,
apiParameters ,
runnableTools ,
toolExecutor ,
currentAssistantContent ,
requestedSecret ,
providerConfidence ,
token ) )
yield return content ;
yield break ;
}
2024-06-30 18:56:08 +00:00
// Prepare the Anthropic HTTP chat request:
var chatRequest = JsonSerializer . Serialize ( new ChatRequest
{
Model = chatModel . Id ,
// Build the messages:
2025-12-10 12:48:13 +00:00
Messages = [ . . messages ] ,
2024-06-30 18:56:08 +00:00
2026-01-18 19:36:04 +00:00
System = chatThread . PrepareSystemPrompt ( settingsManager ) ,
2026-03-12 11:11:54 +00:00
MaxTokens = maxTokens ,
2024-06-30 18:56:08 +00:00
// Right now, we only support streaming completions:
Stream = true ,
2025-11-13 17:13:16 +00:00
AdditionalApiParameters = apiParameters
2024-06-30 18:56:08 +00:00
} , JSON_SERIALIZER_OPTIONS ) ;
2025-01-01 14:49:27 +00:00
2025-01-04 13:11:32 +00:00
async Task < HttpRequestMessage > RequestBuilder ( )
2025-01-01 14:49:27 +00:00
{
2025-01-04 13:11:32 +00:00
// Build the HTTP post request:
var request = new HttpRequestMessage ( HttpMethod . Post , "messages" ) ;
2025-01-01 14:49:27 +00:00
2025-01-04 13:11:32 +00:00
// Set the authorization header:
request . Headers . Add ( "x-api-key" , await requestedSecret . Secret . Decrypt ( ENCRYPTION ) ) ;
2025-01-01 14:49:27 +00:00
2025-01-04 13:11:32 +00:00
// Set the Anthropic version:
request . Headers . Add ( "anthropic-version" , "2023-06-01" ) ;
2025-01-01 14:49:27 +00:00
2025-01-04 13:11:32 +00:00
// Set the content:
request . Content = new StringContent ( chatRequest , Encoding . UTF8 , "application/json" ) ;
return request ;
2025-01-04 11:37:49 +00:00
}
2024-06-30 18:56:08 +00:00
2025-09-03 08:08:04 +00:00
await foreach ( var content in this . StreamChatCompletionInternal < ResponseStreamLine , NoChatCompletionAnnotationStreamLine > ( "Anthropic" , RequestBuilder , token ) )
2025-01-04 13:11:32 +00:00
yield return content ;
2024-06-30 18:56:08 +00:00
}
2026-06-03 21:22:38 +00:00
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 ,
2026-06-05 08:32:53 +00:00
InputSchema = NormalizeInputSchemaForAnthropic ( x . Definition . Function . Parameters ) ,
2026-06-03 21:22:38 +00:00
} )
. 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 + + ;
2026-06-10 09:29:23 +00:00
if ( toolCallCount > ToolSelectionRules . MAX_TOOL_CALLS )
2026-06-03 21:22:38 +00:00
{
2026-06-10 09:29:23 +00:00
var limitMessage = ToolSelectionRules . GetMaxToolCallsLimitMessage ( ) ;
2026-06-03 21:22:38 +00:00
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 ) ;
}
2024-06-30 18:56:08 +00:00
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
2024-12-03 14:24:40 +00:00
public override async IAsyncEnumerable < ImageURL > StreamImageCompletion ( Model imageModel , string promptPositive , string promptNegative = FilterOperator . String . Empty , ImageURL referenceImageURL = default , [ EnumeratorCancellation ] CancellationToken token = default )
2024-06-30 18:56:08 +00:00
{
yield break ;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
2026-01-11 15:02:28 +00:00
/// <inheritdoc />
2026-05-23 09:25:18 +00:00
public override Task < TranscriptionResult > TranscribeAudioAsync ( Model transcriptionModel , string audioFilePath , SettingsManager settingsManager , CancellationToken token = default )
2026-01-11 15:02:28 +00:00
{
2026-05-23 09:25:18 +00:00
return Task . FromResult ( TranscriptionResult . Failure ( ) ) ;
2026-01-11 15:02:28 +00:00
}
2026-02-20 14:32:54 +00:00
/// <inhertidoc />
public override Task < IReadOnlyList < IReadOnlyList < float > > > EmbedTextAsync ( Model embeddingModel , SettingsManager settingsManager , CancellationToken token = default , params List < string > texts )
{
return Task . FromResult < IReadOnlyList < IReadOnlyList < float > > > ( [ ] ) ;
}
2024-06-30 18:56:08 +00:00
/// <inheritdoc />
2026-04-14 11:39:11 +00:00
public override async Task < ModelLoadResult > GetTextModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2024-06-30 18:56:08 +00:00
{
2025-02-24 19:52:32 +00:00
var additionalModels = new [ ]
2024-06-30 18:56:08 +00:00
{
2025-05-25 13:53:47 +00:00
new Model ( "claude-opus-4-0" , "Claude Opus 4.0 (Latest)" ) ,
new Model ( "claude-sonnet-4-0" , "Claude Sonnet 4.0 (Latest)" ) ,
2025-02-24 19:52:32 +00:00
new Model ( "claude-3-7-sonnet-latest" , "Claude 3.7 Sonnet (Latest)" ) ,
new Model ( "claude-3-5-sonnet-latest" , "Claude 3.5 Sonnet (Latest)" ) ,
new Model ( "claude-3-5-haiku-latest" , "Claude 3.5 Haiku (Latest)" ) ,
new Model ( "claude-3-opus-latest" , "Claude 3 Opus (Latest)" ) ,
} ;
2026-04-14 11:39:11 +00:00
var result = await this . LoadModels ( SecretStoreType . LLM_PROVIDER , token , apiKeyProvisional ) ;
return result with
{
Models = [ . . result . Models . Concat ( additionalModels ) . OrderBy ( x = > x . Id ) ]
} ;
2024-06-30 18:56:08 +00:00
}
/// <inheritdoc />
2026-04-14 11:39:11 +00:00
public override Task < ModelLoadResult > GetImageModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2024-12-03 14:24:40 +00:00
{
2026-04-14 11:39:11 +00:00
return Task . FromResult ( ModelLoadResult . FromModels ( [ ] ) ) ;
2024-12-03 14:24:40 +00:00
}
/// <inheritdoc />
2026-04-14 11:39:11 +00:00
public override Task < ModelLoadResult > GetEmbeddingModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2024-06-30 18:56:08 +00:00
{
2026-04-14 11:39:11 +00:00
return Task . FromResult ( ModelLoadResult . FromModels ( [ ] ) ) ;
2024-06-30 18:56:08 +00:00
}
2025-05-11 10:51:35 +00:00
2026-01-09 11:45:21 +00:00
/// <inheritdoc />
2026-04-14 11:39:11 +00:00
public override Task < ModelLoadResult > GetTranscriptionModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2026-01-09 11:45:21 +00:00
{
2026-04-14 11:39:11 +00:00
return Task . FromResult ( ModelLoadResult . FromModels ( [ ] ) ) ;
2026-01-09 11:45:21 +00:00
}
2024-06-30 18:56:08 +00:00
#endregion
2025-02-24 19:52:32 +00:00
2026-04-14 11:39:11 +00:00
private Task < ModelLoadResult > LoadModels ( SecretStoreType storeType , CancellationToken token , string? apiKeyProvisional = null )
2025-02-24 19:52:32 +00:00
{
2026-04-14 11:39:11 +00:00
return this . LoadModelsResponse < ModelsResponse > (
storeType ,
"models?limit=100" ,
modelResponse = > modelResponse . Data ,
token ,
apiKeyProvisional ,
failureReasonSelector : ( response , _ ) = > response . StatusCode switch
2025-02-24 19:52:32 +00:00
{
2026-04-14 11:39:11 +00:00
System . Net . HttpStatusCode . Unauthorized = > ModelLoadFailureReason . INVALID_OR_MISSING_API_KEY ,
System . Net . HttpStatusCode . Forbidden = > ModelLoadFailureReason . AUTHENTICATION_OR_PERMISSION_ERROR ,
2026-05-25 15:32:54 +00:00
System . Net . HttpStatusCode . TooManyRequests = > ModelLoadFailureReason . TOO_MANY_REQUESTS ,
2026-04-14 11:39:11 +00:00
_ = > ModelLoadFailureReason . PROVIDER_UNAVAILABLE ,
} ,
requestConfigurator : ( request , secretKey ) = >
{
request . Headers . Add ( "x-api-key" , secretKey ) ;
request . Headers . Add ( "anthropic-version" , "2023-06-01" ) ;
} ,
jsonSerializerOptions : JSON_SERIALIZER_OPTIONS ) ;
2025-02-24 19:52:32 +00:00
}
2026-06-05 08:32:53 +00:00
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 ,
} ;
2026-06-03 21:22:38 +00:00
}