2024-06-30 20:56:08 +02:00
using System.Runtime.CompilerServices ;
using System.Text ;
using System.Text.Json ;
using AIStudio.Chat ;
using AIStudio.Provider.OpenAI ;
2025-01-02 14:50:54 +01:00
using AIStudio.Settings ;
2026-09-04 15:48:07 +02:00
using AIStudio.Tools.Rust ;
using AIStudio.Tools.ToolCallingSystem ;
using AIStudio.Tools.ToolCallingSystem.Harness ;
2024-06-30 20:56:08 +02:00
namespace AIStudio.Provider.Anthropic ;
2026-05-31 18:46:54 +02:00
public sealed class ProviderAnthropic () : BaseProvider ( LLMProviders . ANTHROPIC , new Uri ( "https://api.anthropic.com/v1/" ), ExternalHttpTrustPolicy . SYSTEM_TRUST_ONLY , LOGGER )
2024-06-30 20:56:08 +02:00
{
2025-09-03 21:25:17 +02:00
private static readonly ILogger < ProviderAnthropic > LOGGER = Program . LOGGER_FACTORY . CreateLogger < ProviderAnthropic >();
2024-06-30 20:56:08 +02:00
#region Implementation of IProvider
2026-04-16 11:24:22 +02:00
/// <inheritdoc />
2026-06-20 15:55:09 +02:00
public override string Id => LLMProviders . ANTHROPIC . ToSecretId ();
2024-06-30 20:56:08 +02:00
2026-04-16 11:24:22 +02:00
/// <inheritdoc />
2024-12-03 15:24:40 +01:00
public override string InstanceName { get ; set ; } = "Anthropic" ;
2024-06-30 20:56:08 +02:00
2026-04-16 11:24:22 +02:00
/// <inheritdoc />
public override bool HasModelLoadingCapability => true ;
2024-06-30 20:56:08 +02:00
/// <inheritdoc />
2025-08-31 14:27:35 +02:00
public override async IAsyncEnumerable < ContentStreamChunk > StreamChatCompletion ( Model chatModel , ChatThread chatThread , SettingsManager settingsManager , [ EnumeratorCancellation ] CancellationToken token = default )
2024-06-30 20:56:08 +02:00
{
// Get the API key:
2026-06-10 21:01:27 +02:00
var requestedSecret = await Program . RUST_SERVICE . GetAPIKey ( this , SecretStoreType . LLM_PROVIDER );
2024-06-30 20:56:08 +02:00
if (! requestedSecret . Success )
yield break ;
2025-11-13 18:13:16 +01:00
// Parse the API parameters:
var apiParameters = this . ParseAdditionalApiParameters ( "system" );
2026-03-12 12:11:54 +01:00
var maxTokens = 4_096 ;
if ( TryPopIntParameter ( apiParameters , "max_tokens" , out var parsedMaxTokens ))
maxTokens = parsedMaxTokens ;
2024-06-30 20:56:08 +02:00
2025-12-10 13:48:13 +01:00
// Build the list of messages:
2025-12-30 18:30:32 +01:00
var messages = await chatThread . Blocks . BuildMessagesAsync (
2026-09-13 14:17:25 +02:00
this . CreateSettingsProvider ( chatModel ),
2025-12-30 18:30:32 +01:00
// Anthropic-specific role mapping:
role => role switch
2025-12-10 13:48:13 +01:00
{
ChatRole . USER => "user" ,
ChatRole . AI => "assistant" ,
ChatRole . AGENT => "assistant" ,
_ => "user" ,
},
2025-12-30 18:30:32 +01: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 13:48:13 +01:00
{
2025-12-30 18:30:32 +01:00
Source = new SubContentBase64Image
{
Data = await attachment . TryAsBase64 ( token : token ) is ( true , var base64Content )
? base64Content
: string . Empty ,
MediaType = attachment . DetermineMimeType (),
}
2025-12-10 13:48:13 +01:00
}
2025-12-30 18:30:32 +01:00
);
2026-09-04 15:48:07 +02:00
//
// Prepare the tools we want to use. When the model may call one, the conversation runs
// through the harness instead of being streamed straight away: tool rounds are not
// streamed, only the final answer is.
//
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 ;
2026-09-14 19:54:23 +02:00
currentAssistantContent ?. BeginToolRun ();
2026-09-04 15:48:07 +02:00
var providerSettings = this . CreateSettingsProvider ( chatModel );
var runnableTools = toolRegistry is null
? []
: await toolRegistry . GetRunnableToolsAsync ( providerSettings , chatThread . RuntimeComponent , chatThread . RuntimeSelectedToolIds ,
this . Provider . GetConfidence ( settingsManager ). Level , chatThread . MayRunTools ( settingsManager ));
var systemPrompt = chatThread . PrepareSystemPrompt ( settingsManager , runnableTools . Select ( x => x . Definition ));
if ( toolExecutor is not null && runnableTools . Count > 0 )
{
var adapter = new AnthropicToolCallingAdapter ( chatModel , [.. messages ], systemPrompt , maxTokens , apiParameters , runnableTools ,
( requestDto , requestToken ) => this . ExecuteMessagesRequest ( requestDto , requestedSecret , requestToken ));
var loop = Program . SERVICE_PROVIDER . GetRequiredService < IToolCallingLoop >();
var loopContext = new ToolCallingLoopContext
{
ChatThread = chatThread ,
RunnableTools = runnableTools ,
ToolExecutor = toolExecutor ,
Provider = this ,
CurrentAssistantContent = currentAssistantContent ,
ProviderInstanceName = this . InstanceName ,
ProviderType = this . Provider ,
ModelId = chatModel . Id ,
};
await foreach ( var content in loop . RunAsync ( adapter , loopContext , token ))
yield return content ;
yield break ;
}
2024-06-30 20:56:08 +02:00
// Prepare the Anthropic HTTP chat request:
var chatRequest = JsonSerializer . Serialize ( new ChatRequest
{
Model = chatModel . Id ,
2026-09-04 15:48:07 +02:00
2024-06-30 20:56:08 +02:00
// Build the messages:
2025-12-10 13:48:13 +01:00
Messages = [.. messages ],
2026-09-04 15:48:07 +02:00
System = systemPrompt ,
2026-03-12 12:11:54 +01:00
MaxTokens = maxTokens ,
2026-09-04 15:48:07 +02:00
2024-06-30 20:56:08 +02:00
// Right now, we only support streaming completions:
Stream = true ,
2025-11-13 18:13:16 +01:00
AdditionalApiParameters = apiParameters
2024-06-30 20:56:08 +02:00
}, JSON_SERIALIZER_OPTIONS );
2025-01-01 15:49:27 +01:00
2025-01-04 14:11:32 +01:00
async Task < HttpRequestMessage > RequestBuilder ()
2025-01-01 15:49:27 +01:00
{
2025-01-04 14:11:32 +01:00
// Build the HTTP post request:
var request = new HttpRequestMessage ( HttpMethod . Post , "messages" );
2025-01-01 15:49:27 +01:00
2025-01-04 14:11:32 +01:00
// Set the authorization header:
2026-06-10 21:01:27 +02:00
request . Headers . Add ( "x-api-key" , await requestedSecret . Secret . Decrypt ( Program . ENCRYPTION ));
2025-01-01 15:49:27 +01:00
2025-01-04 14:11:32 +01:00
// Set the Anthropic version:
request . Headers . Add ( "anthropic-version" , "2023-06-01" );
2025-01-01 15:49:27 +01:00
2025-01-04 14:11:32 +01:00
// Set the content:
request . Content = new StringContent ( chatRequest , Encoding . UTF8 , "application/json" );
return request ;
2025-01-04 12:37:49 +01:00
}
2024-06-30 20:56:08 +02:00
2025-09-03 10:08:04 +02:00
await foreach ( var content in this . StreamChatCompletionInternal < ResponseStreamLine , NoChatCompletionAnnotationStreamLine >( "Anthropic" , RequestBuilder , token ))
2025-01-04 14:11:32 +01:00
yield return content ;
2024-06-30 20:56:08 +02:00
}
2026-09-04 15:48:07 +02:00
/// <summary>
/// Runs one non-streamed messages request, as the tool rounds need it.
/// </summary>
/// <remarks>
/// Tool rounds are not streamed: the whole answer has to be there before its tool calls can
/// be executed. Only the final answer reaches the user through the streaming path.
/// </remarks>
/// <returns>The answer, or null when the request failed and the user was already told.</returns>
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 ( Program . 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 messages request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'." , response . StatusCode , responseBody );
await ToolCallingMessages . SendToolCallingRequestFailedAsync (( int ) response . StatusCode );
return null ;
}
return await response . Content . ReadFromJsonAsync < AnthropicResponse >( JSON_SERIALIZER_OPTIONS , token );
}
2024-06-30 20:56:08 +02:00
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
2024-12-03 15:24:40 +01: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 20:56:08 +02:00
{
yield break ;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
2026-01-11 16:02:28 +01:00
/// <inheritdoc />
2026-05-23 11:25:18 +02:00
public override Task < TranscriptionResult > TranscribeAudioAsync ( Model transcriptionModel , string audioFilePath , SettingsManager settingsManager , CancellationToken token = default )
2026-01-11 16:02:28 +01:00
{
2026-05-23 11:25:18 +02:00
return Task . FromResult ( TranscriptionResult . Failure ());
2026-01-11 16:02:28 +01:00
}
2026-02-20 15:32:54 +01:00
/// <inhertidoc />
public override Task < IReadOnlyList < IReadOnlyList < float >>> EmbedTextAsync ( Model embeddingModel , SettingsManager settingsManager , CancellationToken token = default , params List < string > texts )
{
2026-09-09 18:43:37 +02:00
throw this . CreateEmbeddingsNotSupportedException ();
2026-02-20 15:32:54 +01:00
}
2024-06-30 20:56:08 +02:00
/// <inheritdoc />
2026-04-14 13:39:11 +02:00
public override async Task < ModelLoadResult > GetTextModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2024-06-30 20:56:08 +02:00
{
2025-02-24 20:52:32 +01:00
var additionalModels = new []
2024-06-30 20:56:08 +02:00
{
2025-05-25 15:53:47 +02: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 20:52:32 +01: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-09-04 15:48:07 +02:00
var result = await this . LoadModels ( SecretStoreType . LLM_PROVIDER , apiKeyProvisional , token );
2026-04-14 13:39:11 +02:00
return result with
{
2026-08-29 20:45:39 +02:00
// The API is the authority: when it reports a model we also keep as a fallback above,
// its entry comes first and the fallback is dropped.
Models = [.. result . Models . Concat ( additionalModels ). DistinctBy ( x => x . Id ). OrderBy ( x => x . Id )]
2026-04-14 13:39:11 +02:00
};
2024-06-30 20:56:08 +02:00
}
/// <inheritdoc />
2026-04-14 13:39:11 +02:00
public override Task < ModelLoadResult > GetImageModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2024-12-03 15:24:40 +01:00
{
2026-04-14 13:39:11 +02:00
return Task . FromResult ( ModelLoadResult . FromModels ([]));
2024-12-03 15:24:40 +01:00
}
/// <inheritdoc />
2026-04-14 13:39:11 +02:00
public override Task < ModelLoadResult > GetEmbeddingModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2024-06-30 20:56:08 +02:00
{
2026-04-14 13:39:11 +02:00
return Task . FromResult ( ModelLoadResult . FromModels ([]));
2024-06-30 20:56:08 +02:00
}
2025-05-11 12:51:35 +02:00
2026-01-09 12:45:21 +01:00
/// <inheritdoc />
2026-04-14 13:39:11 +02:00
public override Task < ModelLoadResult > GetTranscriptionModels ( string? apiKeyProvisional = null , CancellationToken token = default )
2026-01-09 12:45:21 +01:00
{
2026-04-14 13:39:11 +02:00
return Task . FromResult ( ModelLoadResult . FromModels ([]));
2026-01-09 12:45:21 +01:00
}
2024-06-30 20:56:08 +02:00
#endregion
2026-09-04 15:48:07 +02:00
private Task < ModelLoadResult > LoadModels ( SecretStoreType storeType , string? apiKeyProvisional , CancellationToken token )
2025-02-24 20:52:32 +01:00
{
2026-04-14 13:39:11 +02:00
return this . LoadModelsResponse < ModelsResponse >(
storeType ,
"models?limit=100" ,
modelResponse => modelResponse . Data ,
apiKeyProvisional ,
failureReasonSelector : ( response , _ ) => response . StatusCode switch
2025-02-24 20:52:32 +01:00
{
2026-04-14 13:39:11 +02:00
System . Net . HttpStatusCode . Unauthorized => ModelLoadFailureReason . INVALID_OR_MISSING_API_KEY ,
System . Net . HttpStatusCode . Forbidden => ModelLoadFailureReason . AUTHENTICATION_OR_PERMISSION_ERROR ,
2026-05-25 17:32:54 +02:00
System . Net . HttpStatusCode . TooManyRequests => ModelLoadFailureReason . TOO_MANY_REQUESTS ,
2026-04-14 13:39:11 +02:00
_ => ModelLoadFailureReason . PROVIDER_UNAVAILABLE ,
},
requestConfigurator : ( request , secretKey ) =>
{
request . Headers . Add ( "x-api-key" , secretKey );
request . Headers . Add ( "anthropic-version" , "2023-06-01" );
},
2026-09-04 15:48:07 +02:00
jsonSerializerOptions : JSON_SERIALIZER_OPTIONS , token : token );
2025-02-24 20:52:32 +01:00
}
2026-04-14 13:39:11 +02:00
}