using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using AnthropicClient.Json;
using AnthropicClient.Utils;
using AnthropicClient.Models;
namespace AnthropicClient;
///
/// Represents a client for interacting with the Anthropic API.
///
public interface IAnthropicApiClient
{
///
/// Creates a chat message asynchronously.
///
/// The chat message request to create.
/// A task that represents the asynchronous operation. The task result contains the chat response as an .
Task> CreateChatMessageAsync(ChatMessageRequest request);
///
/// Creates a chat message asynchronously and streams the response.
///
/// The chat message request to create.
/// An asynchronous enumerable that yields the chat response line by line.
IAsyncEnumerable CreateChatMessageAsync(StreamChatMessageRequest request);
}
///
public class AnthropicApiClient : IAnthropicApiClient
{
private const string BaseUrl = "https://api.anthropic.com/v1/";
private const string ApiKeyHeader = "x-api-key";
private const string MessagesEndpoint = "messages";
private const string JsonContentType = "application/json";
private const string EventPrefix = "event:";
private const string DataPrefix = "data:";
private readonly Dictionary _defaultHeaders = new()
{
{ "anthropic-version", "2023-06-01" },
};
private readonly HttpClient _httpClient;
///
/// Initializes a new instance of the class.
///
/// The API key to use for the client.
/// The HTTP client to use for the client.
/// Thrown when the API key or HTTP client is null.
/// A new instance of the class.
public AnthropicApiClient(string apiKey, HttpClient httpClient)
{
ArgumentValidator.ThrowIfNull(apiKey, nameof(apiKey));
ArgumentValidator.ThrowIfNull(httpClient, nameof(httpClient));
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri(BaseUrl);
_httpClient.DefaultRequestHeaders.Add(ApiKeyHeader, apiKey);
_httpClient.DefaultRequestHeaders
.Accept
.Add(new MediaTypeWithQualityHeaderValue(JsonContentType));
foreach (var pair in _defaultHeaders)
{
_httpClient.DefaultRequestHeaders.Add(pair.Key, pair.Value);
}
}
///
public async Task> CreateChatMessageAsync(ChatMessageRequest request)
{
var response = await SendRequestAsync(request);
var anthropicHeaders = new AnthropicHeaders(response.Headers);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode is false)
{
var error = Deserialize(responseContent) ?? new AnthropicError();
return AnthropicResult.Failure(error, anthropicHeaders);
}
var chatResponse = Deserialize(responseContent) ?? new ChatResponse();
if (request.Tools is not null && request.Tools.Count > 0)
{
chatResponse.ToolCall = GetToolCall(chatResponse, request.Tools);
}
return AnthropicResult.Success(chatResponse, anthropicHeaders);
}
///
public async IAsyncEnumerable CreateChatMessageAsync(StreamChatMessageRequest request)
{
var response = await SendRequestAsync(request);
var anthropicHeaders = new AnthropicHeaders(response.Headers);
using var responseContent = await response.Content.ReadAsStreamAsync();
using var streamReader = new StreamReader(responseContent);
ChatResponse? chatResponse = null;
Content? content = null;
var toolInputJsonStringBuilder = new StringBuilder();
var currentEvent = new AnthropicEvent();
do
{
var line = await streamReader.ReadLineAsync();
// I know...this is not pretty, but here is why...
// as events are being yielded I want to also
// build up the complete chat response
// so I can yield it as a special event to make tool
// calling easier to handle
// initialize chat response on message start
if (currentEvent.Type is EventType.MessageStart && currentEvent.Data is MessageStartEventData msgStartData)
{
chatResponse = msgStartData.Message;
}
// initialize content block on content block start
if (currentEvent.Type is EventType.ContentBlockStart && currentEvent.Data is ContentStartEventData contentStartData)
{
content = contentStartData.ContentBlock;
}
// update content block with deltas based on
// current content type and delta type
if (currentEvent.Type is EventType.ContentBlockDelta && currentEvent.Data is ContentDeltaEventData contentDeltaData)
{
if (content is TextContent textContent && contentDeltaData.Delta is TextDelta textDelta)
{
var newText = textContent.Text + textDelta.Text;
content = new TextContent(newText);
}
if (content is ToolUseContent toolUseContent && contentDeltaData.Delta is JsonDelta jsonDelta)
{
toolInputJsonStringBuilder.Append(jsonDelta.PartialJson);
}
}
// finalize content block on content block stop
// and add it to the chat response
if (currentEvent.Type is EventType.ContentBlockStop)
{
if (content is not null && chatResponse is not null)
{
if (content is TextContent textContent)
{
chatResponse.Content.Add(textContent);
}
if (content is ToolUseContent toolUseContent)
{
var input = Deserialize>(toolInputJsonStringBuilder.ToString());
var newToolUseContent = new ToolUseContent()
{
Id = toolUseContent.Id,
Name = toolUseContent.Name,
Input = input!,
};
chatResponse.Content.Add(newToolUseContent);
}
content = null;
}
}
// update chat response with message delta data
if (
currentEvent.Type is EventType.MessageDelta &&
currentEvent.Data is MessageDeltaEventData msgDeltaData &&
chatResponse is not null
)
{
var existingUsage = chatResponse.Usage;
var newUsage = new ChatUsage()
{
InputTokens = existingUsage.InputTokens + msgDeltaData.Usage.InputTokens,
OutputTokens = existingUsage.OutputTokens + msgDeltaData.Usage.OutputTokens,
};
chatResponse = new ChatResponse()
{
Id = chatResponse.Id,
Model = chatResponse.Model,
Role = chatResponse.Role,
StopReason = msgDeltaData.Delta.StopReason,
StopSequence = msgDeltaData.Delta.StopSequence,
Type = chatResponse.Type,
Usage = newUsage,
Content = chatResponse.Content,
};
if (request.Tools is not null && request.Tools.Count > 0)
{
chatResponse.ToolCall = GetToolCall(chatResponse, request.Tools);
}
}
// yield chat response on message stop
if (currentEvent.Type is EventType.MessageStop && chatResponse is not null)
{
var eventData = new MessageCompleteEventData(chatResponse, anthropicHeaders);
yield return new AnthropicEvent(EventType.MessageComplete, eventData);
chatResponse = null;
}
if (line is null)
{
break;
}
if (line == string.Empty)
{
yield return currentEvent;
currentEvent = new AnthropicEvent();
}
if (line.StartsWith(EventPrefix))
{
var eventType = line.Substring(EventPrefix.Length).Trim();
currentEvent = new AnthropicEvent(eventType, currentEvent.Data);
continue;
}
if (line.StartsWith(DataPrefix))
{
var eventData = line.Substring(DataPrefix.Length).Trim();
var eventDataJson = Deserialize(eventData);
currentEvent = new AnthropicEvent(currentEvent.Type, eventDataJson!);
continue;
}
} while (true);
}
private ToolCall? GetToolCall(ChatResponse response, List tools)
{
var toolUse = response.Content.OfType().FirstOrDefault();
if (toolUse is null)
{
return null;
}
var tool = tools.FirstOrDefault(t => t.Name == toolUse.Name);
if (tool is null)
{
return null;
}
return new ToolCall(tool, toolUse);
}
private async Task SendRequestAsync(MessageRequest request)
{
var requestContent = new StringContent(Serialize(request), Encoding.UTF8, JsonContentType);
return await _httpClient.PostAsync(MessagesEndpoint, requestContent);
}
private string Serialize(T obj) => JsonSerializer.Serialize(obj, JsonSerializationOptions.DefaultOptions);
private T? Deserialize(string json) => JsonSerializer.Deserialize(json, JsonSerializationOptions.DefaultOptions);
}