using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using AnthropicClient.Json; using AnthropicClient.Models; using AnthropicClient.Utils; namespace AnthropicClient; /// 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 string CountTokensEndpoint => $"{MessagesEndpoint}/count_tokens"; private string MessageBatchesEndpoint => $"{MessagesEndpoint}/batches"; private const string ModelsEndpoint = "models"; private const string FilesEndpoint = "files"; 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> CreateMessageAsync(MessageRequest request, CancellationToken cancellationToken = default) { var response = await SendRequestAsync(MessagesEndpoint, request, cancellationToken); 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 msgResponse = Deserialize(responseContent) ?? new MessageResponse(); if (request.Tools is not null && request.Tools.Count > 0) { msgResponse.ToolCall = GetToolCall(msgResponse, request.Tools); } return AnthropicResult.Success(msgResponse, anthropicHeaders); } /// public async IAsyncEnumerable CreateMessageAsync(StreamMessageRequest request, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var response = await SendRequestAsync(MessagesEndpoint, request, cancellationToken); if (response.IsSuccessStatusCode is false) { var errorContent = await response.Content.ReadAsStringAsync(); var error = Deserialize(errorContent) ?? new AnthropicError(); yield return new AnthropicEvent(EventType.Error, new ErrorEventData(error.Error)); yield break; } var anthropicHeaders = new AnthropicHeaders(response.Headers); using var responseContent = await response.Content.ReadAsStreamAsync(); using var streamReader = new StreamReader(responseContent); MessageResponse? msgResponse = 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 response // so I can yield it as a special event to make tool // calling easier to handle // initialize response on message start if (currentEvent.Type is EventType.MessageStart && currentEvent.Data is MessageStartEventData msgStartData) { msgResponse = 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) { if (contentDeltaData.Delta is TextDelta textDelta) { var newText = textContent.Text + textDelta.Text; content = new TextContent(newText) { Citations = textContent.Citations, }; } if (contentDeltaData.Delta is CitationDelta citationDelta) { var citations = new List() { citationDelta.Citation, }; if (textContent.Citations is not null) { citations.AddRange(textContent.Citations); } var newContent = new TextContent(textContent.Text) { Citations = [.. citations], }; content = newContent; } } 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 response if (currentEvent.Type is EventType.ContentBlockStop) { if (content is not null && msgResponse is not null) { if (content is TextContent textContent) { msgResponse.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!, }; msgResponse.Content.Add(newToolUseContent); } content = null; } } // update response with message delta data if ( currentEvent.Type is EventType.MessageDelta && currentEvent.Data is MessageDeltaEventData msgDeltaData && msgResponse is not null ) { var existingUsage = msgResponse.Usage; var newUsage = new Usage() { InputTokens = existingUsage.InputTokens + msgDeltaData.Usage.InputTokens, OutputTokens = existingUsage.OutputTokens + msgDeltaData.Usage.OutputTokens, CacheCreationInputTokens = existingUsage.CacheCreationInputTokens + msgDeltaData.Usage.CacheCreationInputTokens, CacheReadInputTokens = existingUsage.CacheReadInputTokens + msgDeltaData.Usage.CacheReadInputTokens, }; msgResponse = new MessageResponse() { Id = msgResponse.Id, Model = msgResponse.Model, Role = msgResponse.Role, StopReason = msgDeltaData.Delta.StopReason, StopSequence = msgDeltaData.Delta.StopSequence, Type = msgResponse.Type, Usage = newUsage, Content = msgResponse.Content, }; if (request.Tools is not null && request.Tools.Count > 0) { msgResponse.ToolCall = GetToolCall(msgResponse, request.Tools); } } // yield response on message stop if (currentEvent.Type is EventType.MessageStop && msgResponse is not null) { var eventData = new MessageCompleteEventData(msgResponse, anthropicHeaders); yield return new AnthropicEvent(EventType.MessageComplete, eventData); msgResponse = null; } if (line is null) { if (string.IsNullOrWhiteSpace(currentEvent.Type) is false) { yield return currentEvent; currentEvent = new AnthropicEvent(); } 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); } /// public async Task> CreateMessageBatchAsync(MessageBatchRequest request, CancellationToken cancellationToken = default) { var response = await SendRequestAsync(MessageBatchesEndpoint, request, cancellationToken); return await CreateResultAsync(response); } /// public async Task> GetMessageBatchAsync(string batchId, CancellationToken cancellationToken = default) { var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}", cancellationToken: cancellationToken); return await CreateResultAsync(response); } /// public async Task>> ListMessageBatchesAsync(PagingRequest? request = null, CancellationToken cancellationToken = default) { var pagingRequest = request ?? new PagingRequest(); var endpoint = $"{MessageBatchesEndpoint}?{pagingRequest.ToQueryParameters()}"; var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); return await CreateResultAsync>(response); } /// public async IAsyncEnumerable>> ListAllMessageBatchesAsync(int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var result in GetAllPagesAsync(MessageBatchesEndpoint, limit, cancellationToken)) { yield return result; } } /// public async Task> CancelMessageBatchAsync(string batchId, CancellationToken cancellationToken = default) { var endpoint = $"{MessageBatchesEndpoint}/{batchId}/cancel"; var response = await SendRequestAsync(endpoint, HttpMethod.Post, cancellationToken); return await CreateResultAsync(response); } /// public async Task> DeleteMessageBatchAsync(string batchId, CancellationToken cancellationToken = default) { var endpoint = $"{MessageBatchesEndpoint}/{batchId}"; var response = await SendRequestAsync(endpoint, HttpMethod.Delete, cancellationToken); return await CreateResultAsync(response); } /// public async Task>> GetMessageBatchResultsAsync(string batchId, CancellationToken cancellationToken = default) { var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}/results", cancellationToken: cancellationToken); var anthropicHeaders = new AnthropicHeaders(response.Headers); if (response.IsSuccessStatusCode is false) { var content = await response.Content.ReadAsStringAsync(); var error = Deserialize(content) ?? new AnthropicError(); return AnthropicResult>.Failure(error, anthropicHeaders); } return AnthropicResult>.Success(ReadResultsAsync(), anthropicHeaders); async IAsyncEnumerable ReadResultsAsync() { using var responseContent = await response.Content.ReadAsStreamAsync(); using var streamReader = new StreamReader(responseContent); var line = await streamReader.ReadLineAsync(); while (line is not null) { var resultItem = Deserialize(line) ?? new MessageBatchResultItem(); yield return resultItem; line = await streamReader.ReadLineAsync(); } } } /// public async Task> CountMessageTokensAsync(CountMessageTokensRequest request, CancellationToken cancellationToken = default) { var response = await SendRequestAsync(CountTokensEndpoint, request, cancellationToken); return await CreateResultAsync(response); } /// public async Task>> ListModelsAsync(PagingRequest? request = null, CancellationToken cancellationToken = default) { var pagingRequest = request ?? new PagingRequest(); var endpoint = $"{ModelsEndpoint}?{pagingRequest.ToQueryParameters()}"; var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); return await CreateResultAsync>(response); } /// public async IAsyncEnumerable>> ListAllModelsAsync(int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var result in GetAllPagesAsync(ModelsEndpoint, limit, cancellationToken)) { yield return result; } } /// public async Task> GetModelAsync(string modelId, CancellationToken cancellationToken = default) { var endpoint = $"{ModelsEndpoint}/{modelId}"; var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); return await CreateResultAsync(response); } /// public async Task> CreateFileAsync(CreateFileRequest request, CancellationToken cancellationToken = default) { var response = await SendFileRequestAsync(FilesEndpoint, request, cancellationToken); if (response.IsSuccessStatusCode is false) { var content = await response.Content.ReadAsStringAsync(); var error = Deserialize(content) ?? new AnthropicError(); return AnthropicResult.Failure(error, new AnthropicHeaders(response.Headers)); } return await CreateResultAsync(response); } /// public async Task>> ListFilesAsync(PagingRequest? request = null, CancellationToken cancellationToken = default) { var pagingRequest = request ?? new PagingRequest(); var endpoint = $"{FilesEndpoint}?{pagingRequest.ToQueryParameters()}"; var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); if (response.IsSuccessStatusCode is false) { var content = await response.Content.ReadAsStringAsync(); var error = Deserialize(content) ?? new AnthropicError(); return AnthropicResult>.Failure(error, new AnthropicHeaders(response.Headers)); } return await CreateResultAsync>(response); } /// public async IAsyncEnumerable>> ListAllFilesAsync(int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var result in GetAllPagesAsync(FilesEndpoint, limit, cancellationToken)) { yield return result; } } /// public async Task> GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default) { var endpoint = $"{FilesEndpoint}/{fileId}"; var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); if (response.IsSuccessStatusCode is false) { var content = await response.Content.ReadAsStringAsync(); var error = Deserialize(content) ?? new AnthropicError(); return AnthropicResult.Failure(error, new AnthropicHeaders(response.Headers)); } return await CreateResultAsync(response); } /// public async Task> GetFileAsync(string fileId, CancellationToken cancellationToken = default) { var endpoint = $"{FilesEndpoint}/{fileId}/content"; var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); if (response.IsSuccessStatusCode is false) { var content = await response.Content.ReadAsStringAsync(); var error = Deserialize(content) ?? new AnthropicError(); return AnthropicResult.Failure(error, new AnthropicHeaders(response.Headers)); } var stream = await response.Content.ReadAsStreamAsync(); return AnthropicResult.Success(stream, new AnthropicHeaders(response.Headers)); } /// public async Task> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default) { var endpoint = $"{FilesEndpoint}/{fileId}"; var response = await SendRequestAsync(endpoint, HttpMethod.Delete, cancellationToken); if (response.IsSuccessStatusCode is false) { var content = await response.Content.ReadAsStringAsync(); var error = Deserialize(content) ?? new AnthropicError(); return AnthropicResult.Failure(error, new AnthropicHeaders(response.Headers)); } return await CreateResultAsync(response); } private async IAsyncEnumerable>> GetAllPagesAsync(string endpoint, int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var pagingRequest = new PagingRequest(limit: limit); string Endpoint() => $"{endpoint}?{pagingRequest.ToQueryParameters()}"; bool hasMore; do { var response = await SendRequestAsync(Endpoint(), cancellationToken: cancellationToken); var anthropicHeaders = new AnthropicHeaders(response.Headers); var responseContent = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode is false) { var error = Deserialize(responseContent) ?? new AnthropicError(); yield return AnthropicResult>.Failure(error, anthropicHeaders); yield break; } var page = Deserialize>(responseContent) ?? new Page(); if (page.HasMore && page.LastId is not null) { hasMore = true; pagingRequest = new PagingRequest(limit: limit, afterId: page.LastId); } else { hasMore = false; } yield return AnthropicResult>.Success(page, anthropicHeaders); } while (hasMore); } private ToolCall? GetToolCall(MessageResponse 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> CreateResultAsync(HttpResponseMessage response) where T : new() { 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 model = Deserialize(responseContent) ?? new T(); return AnthropicResult.Success(model, anthropicHeaders); } private async Task SendRequestAsync(string endpoint, HttpMethod? method = null, CancellationToken cancellationToken = default) { var request = new HttpRequestMessage(method ?? HttpMethod.Get, endpoint); return await _httpClient.SendAsync(request, cancellationToken); } private async Task SendRequestAsync(string endpoint, T request, CancellationToken cancellationToken = default) { var requestJson = Serialize(request); var requestContent = new StringContent(requestJson, Encoding.UTF8, JsonContentType); return await _httpClient.PostAsync(endpoint, requestContent, cancellationToken); } private async Task SendFileRequestAsync(string endpoint, CreateFileRequest request, CancellationToken cancellationToken = default) { using var multipartContent = new MultipartFormDataContent(); using var fileContent = new ByteArrayContent(request.File); fileContent.Headers.ContentType = new MediaTypeHeaderValue(request.FileType); multipartContent.Add(fileContent, "file", request.FileName); return await _httpClient.PostAsync(endpoint, multipartContent, cancellationToken); } private string Serialize(T obj) => JsonSerializer.Serialize(obj, JsonSerializationOptions.DefaultOptions); private T? Deserialize(string json) => JsonSerializer.Deserialize(json, JsonSerializationOptions.DefaultOptions); }