diff --git a/README.md b/README.md index dc0901f..f23653a 100644 --- a/README.md +++ b/README.md @@ -971,3 +971,193 @@ foreach (var content in response.Value.Content) } } ``` + +### Message Batches + +Anthropic provides a feature called [Message Batches](https://docs.anthropic.com/en/docs/build-with-claude/message-batches) that allows you to send multiple messages in a single request. This feature is covered in depth in [Anthropic's API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/message-batches). + +#### Create a message batch + +You can create a message batch that will consist of one or more requests to create messages. + +```csharp +using AnthropicClient; +using AnthropicClient.Models; + +var request = new MessageBatchRequest([ + new( + Guid.NewGuid().ToString(), + new( + model: AnthropicModels.Claude3Haiku, + messages: [new(MessageRole.User, [new TextContent("Hello!")])] + ) + ), +]); + +var response = await client.CreateMessageBatchAsync(request); + +if (response.IsFailure) +{ + Console.WriteLine("Failed to create message batch"); + Console.WriteLine("Error Type: {0}", response.Error.Error.Type); + Console.WriteLine("Error Message: {0}", response.Error.Error.Message); + return; +} + +Console.WriteLine("Message Batch Id: {0}", response.Value.Id); +``` + +#### Get a message batch + +You can retrieve a message batch by its id. + +```csharp +using AnthropicClient; +using AnthropicClient.Models; + +var response = await client.GetMessageBatchAsync("batch-id"); + +if (response.IsFailure) +{ + Console.WriteLine("Failed to get message batch"); + Console.WriteLine("Error Type: {0}", response.Error.Error.Type); + Console.WriteLine("Error Message: {0}", response.Error.Error.Message); + return; +} + +Console.WriteLine("Message Batch Id: {0}", response.Value.Id); +``` + +#### Get a message batch results + +You can retrieve the results of a message batch by its id. The results are returned as an `IAsyncEnumerable` collection so that they can be streamed and processed as they are received. + +```csharp +using AnthropicClient; +using AnthropicClient.Models; + +var response = await client.GetMessageBatchResultsAsync("batch-id"); + +if (response.IsFailure) +{ + Console.WriteLine("Failed to get message batch results"); + Console.WriteLine("Error Type: {0}", response.Error.Error.Type); + Console.WriteLine("Error Message: {0}", response.Error.Error.Message); + return; +} + +await foreach (var item in response.Value) +{ + Console.WriteLine("Item Custom Id: {0}", result.CustomId); + + switch (item.Result) + { + case SucceededMessageBatchResult successResult: + foreach (var content in successResult.Message.Content) + { + if (content is TextContent textContent) + { + Console.WriteLine("Message Batch Result: {0}", textContent.Text); + } + } + break; + default: + Console.WriteLine("Message Batch Result: {0}", item.Result.Type); + break; + } +} +``` + +#### List message batches + +You can retrieve a page of message batches. + +```csharp +using AnthropicClient; +using AnthropicClient.Models; + +var response = await client.ListMessageBatchesAsync(); + +if (response.IsFailure) +{ + Console.WriteLine("Failed to list message batches"); + Console.WriteLine("Error Type: {0}", response.Error.Error.Type); + Console.WriteLine("Error Message: {0}", response.Error.Error.Message); + return; +} + +foreach (var batch in response.Value.Data) +{ + Console.WriteLine("Message Batch Id: {0}", batch.Id); +} +``` + +#### List all message batches + +You can also retrieve all the pages of message batches without having to implement pagination yourself. This is done by returning an `IAsyncEnumerable` collection that can be streamed and processed as the pages are received. + +```csharp +using AnthropicClient; +using AnthropicClient.Models; + +var pageResponses = client.ListAllMessageBatchesAsync(); + +await foreach (var response in pageResponses) +{ + if (response.IsFailure) + { + Console.WriteLine("Failed to list message batches"); + Console.WriteLine("Error Type: {0}", response.Error.Error.Type); + Console.WriteLine("Error Message: {0}", response.Error.Error.Message); + return; + } + + foreach (var batch in response.Value.Data) + { + Console.WriteLine("Message Batch Id: {0}", batch.Id); + } +} +``` + +#### Cancel a message batch + +You can cancel a message batch by its id. + +```csharp +using AnthropicClient; +using AnthropicClient.Models; + +var response = await client.CancelMessageBatchAsync("batch-id"); + +if (response.IsFailure) +{ + Console.WriteLine("Failed to cancel message batch"); + Console.WriteLine("Error Type: {0}", response.Error.Error.Type); + Console.WriteLine("Error Message: {0}", response.Error.Error.Message); + return; +} + +Console.WriteLine("Message Batch Id: {0}", response.Value.Id); +Console.WriteLine("Message Batch Status: {0}", response.Value.ProcessingStatus); +``` + +#### Delete a message batch + +You can delete a message batch that is no longer being processed by its id. + +```csharp +using AnthropicClient; +using AnthropicClient.Models; + +var response = await client.DeleteMessageBatchAsync("batch-id"); + +if (response.IsFailure) +{ + Console.WriteLine("Failed to delete message batch"); + Console.WriteLine("Error Type: {0}", response.Error.Error.Type); + Console.WriteLine("Error Message: {0}", response.Error.Error.Message); + return; +} + +Console.WriteLine("Message Batch Id: {0}", response.Value.Id); +``` diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs index 3be31ee..826b735 100644 --- a/src/AnthropicClient/AnthropicApiClient.cs +++ b/src/AnthropicClient/AnthropicApiClient.cs @@ -1,7 +1,6 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; -using System.Threading.Tasks; using AnthropicClient.Json; using AnthropicClient.Models; @@ -9,62 +8,14 @@ using AnthropicClient.Utils; namespace AnthropicClient; -/// -/// Represents a client for interacting with the Anthropic API. -/// -public interface IAnthropicApiClient -{ - /// - /// Creates a message asynchronously. - /// - /// The message request to create. - /// A task that represents the asynchronous operation. The task result contains the response as an . - Task> CreateMessageAsync(MessageRequest request); - - /// - /// Creates a message asynchronously and streams the response. - /// - /// The message request to create. - /// An asynchronous enumerable that yields the response event by event. - IAsyncEnumerable CreateMessageAsync(StreamMessageRequest request); - - /// - /// Counts the tokens in a message asynchronously. - /// - /// The count message tokens request. - /// A task that represents the asynchronous operation. The task result contains the response as an where T is . - Task> CountMessageTokensAsync(CountMessageTokensRequest request); - - /// - /// Lists the models asynchronously. - /// - /// The paging request to use for listing the models. - /// A task that represents the asynchronous operation. The task result contains the response as an where T is where T is . - Task>> ListModelsAsync(PagingRequest? request = null); - - /// - /// Lists the models asynchronously - /// - /// The maximum number of models to return in each page. - /// An asynchronous enumerable that yields the response as an where T is where T is . - /// - IAsyncEnumerable>> ListAllModelsAsync(int limit = 20); - - /// - /// Gets a model by its ID asynchronously. - /// - /// The ID of the model to get. - /// A task that represents the asynchronous operation. The task result contains the response as an where T is . - Task> GetModelAsync(string modelId); -} - /// 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 CountTokensEndpoint = "messages/count_tokens"; + private string CountTokensEndpoint => $"{MessagesEndpoint}/count_tokens"; + private string MessageBatchesEndpoint => $"{MessagesEndpoint}/batches"; private const string ModelsEndpoint = "models"; private const string JsonContentType = "application/json"; private const string EventPrefix = "event:"; @@ -288,20 +239,90 @@ public class AnthropicApiClient : IAnthropicApiClient } /// - public async Task> CountMessageTokensAsync(CountMessageTokensRequest request) + public async Task> CreateMessageBatchAsync(MessageBatchRequest request) { - var response = await SendRequestAsync(CountTokensEndpoint, request); + var response = await SendRequestAsync(MessageBatchesEndpoint, request); + return await CreateResultAsync(response); + } + + /// + public async Task> GetMessageBatchAsync(string batchId) + { + var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}"); + return await CreateResultAsync(response); + } + + /// + public async Task>> ListMessageBatchesAsync(PagingRequest? request = null) + { + var pagingRequest = request ?? new PagingRequest(); + var endpoint = $"{MessageBatchesEndpoint}?{pagingRequest.ToQueryParameters()}"; + var response = await SendRequestAsync(endpoint); + return await CreateResultAsync>(response); + } + + /// + public async IAsyncEnumerable>> ListAllMessageBatchesAsync(int limit = 20) + { + await foreach (var result in GetAllPagesAsync(MessageBatchesEndpoint, limit)) + { + yield return result; + } + } + + /// + public async Task> CancelMessageBatchAsync(string batchId) + { + var endpoint = $"{MessageBatchesEndpoint}/{batchId}/cancel"; + var response = await SendRequestAsync(endpoint, HttpMethod.Post); + return await CreateResultAsync(response); + } + + /// + public async Task> DeleteMessageBatchAsync(string batchId) + { + var endpoint = $"{MessageBatchesEndpoint}/{batchId}"; + var response = await SendRequestAsync(endpoint, HttpMethod.Delete); + return await CreateResultAsync(response); + } + + /// + public async Task>> GetMessageBatchResultsAsync(string batchId) + { + var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}/results"); 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 content = await response.Content.ReadAsStringAsync(); + var error = Deserialize(content) ?? new AnthropicError(); + return AnthropicResult>.Failure(error, anthropicHeaders); } - var msgResponse = Deserialize(responseContent) ?? new TokenCountResponse(); - return AnthropicResult.Success(msgResponse, 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) + { + var response = await SendRequestAsync(CountTokensEndpoint, request); + return await CreateResultAsync(response); } /// @@ -310,24 +331,30 @@ public class AnthropicApiClient : IAnthropicApiClient var pagingRequest = request ?? new PagingRequest(); var endpoint = $"{ModelsEndpoint}?{pagingRequest.ToQueryParameters()}"; var response = await SendRequestAsync(endpoint); - 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 page = Deserialize>(responseContent) ?? new Page(); - return AnthropicResult>.Success(page, anthropicHeaders); + return await CreateResultAsync>(response); } /// public async IAsyncEnumerable>> ListAllModelsAsync(int limit = 20) + { + await foreach (var result in GetAllPagesAsync(ModelsEndpoint, limit)) + { + yield return result; + } + } + + /// + public async Task> GetModelAsync(string modelId) + { + var endpoint = $"{ModelsEndpoint}/{modelId}"; + var response = await SendRequestAsync(endpoint); + return await CreateResultAsync(response); + } + + private async IAsyncEnumerable>> GetAllPagesAsync(string endpoint, int limit = 20) { var pagingRequest = new PagingRequest(limit: limit); - string Endpoint() => $"{ModelsEndpoint}?{pagingRequest.ToQueryParameters()}"; + string Endpoint() => $"{endpoint}?{pagingRequest.ToQueryParameters()}"; bool hasMore; do @@ -339,11 +366,11 @@ public class AnthropicApiClient : IAnthropicApiClient if (response.IsSuccessStatusCode is false) { var error = Deserialize(responseContent) ?? new AnthropicError(); - yield return AnthropicResult>.Failure(error, anthropicHeaders); + yield return AnthropicResult>.Failure(error, anthropicHeaders); yield break; } - var page = Deserialize>(responseContent) ?? new Page(); + var page = Deserialize>(responseContent) ?? new Page(); if (page.HasMore && page.LastId is not null) { @@ -355,28 +382,10 @@ public class AnthropicApiClient : IAnthropicApiClient hasMore = false; } - yield return AnthropicResult>.Success(page, anthropicHeaders); + yield return AnthropicResult>.Success(page, anthropicHeaders); } while (hasMore); } - /// - public async Task> GetModelAsync(string modelId) - { - var endpoint = $"{ModelsEndpoint}/{modelId}"; - var response = await SendRequestAsync(endpoint); - 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 AnthropicModel(); - return AnthropicResult.Success(model, anthropicHeaders); - } - private ToolCall? GetToolCall(MessageResponse response, List tools) { var toolUse = response.Content.OfType().FirstOrDefault(); @@ -396,9 +405,25 @@ public class AnthropicApiClient : IAnthropicApiClient return new ToolCall(tool, toolUse); } - private async Task SendRequestAsync(string endpoint) + private async Task> CreateResultAsync(HttpResponseMessage response) where T : new() { - return await _httpClient.GetAsync(endpoint); + 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) + { + var request = new HttpRequestMessage(method ?? HttpMethod.Get, endpoint); + return await _httpClient.SendAsync(request); } private async Task SendRequestAsync(string endpoint, T request) diff --git a/src/AnthropicClient/IAnthropicApiClient.cs b/src/AnthropicClient/IAnthropicApiClient.cs new file mode 100644 index 0000000..27c7e00 --- /dev/null +++ b/src/AnthropicClient/IAnthropicApiClient.cs @@ -0,0 +1,101 @@ +using AnthropicClient.Models; + +namespace AnthropicClient; + +/// +/// Represents a client for interacting with the Anthropic API. +/// +public interface IAnthropicApiClient +{ + /// + /// Creates a message asynchronously. + /// + /// The message request to create. + /// A task that represents the asynchronous operation. The task result contains the response as an . + Task> CreateMessageAsync(MessageRequest request); + + /// + /// Creates a message asynchronously and streams the response. + /// + /// The message request to create. + /// An asynchronous enumerable that yields the response event by event. + IAsyncEnumerable CreateMessageAsync(StreamMessageRequest request); + + /// + /// Creates a batch of messages asynchronously. + /// + /// The message batch request to create. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is . + Task> CreateMessageBatchAsync(MessageBatchRequest request); + + /// + /// Gets a message batch asynchronously. + /// + /// The ID of the message batch to get. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is . + Task> GetMessageBatchAsync(string batchId); + + /// + /// Lists the message batches asynchronously. + /// + /// The paging request to use for listing the message batches. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is where T is . + Task>> ListMessageBatchesAsync(PagingRequest? request = null); + + /// + /// Lists all message batches asynchronously. + /// + /// The maximum number of message batches to return in each page. + /// An asynchronous enumerable that yields the response as an where T is where T is . + IAsyncEnumerable>> ListAllMessageBatchesAsync(int limit = 20); + + /// + /// Cancels a message batch asynchronously. + /// + /// The ID of the message batch to cancel. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is . + Task> CancelMessageBatchAsync(string batchId); + + /// + /// Deletes a message batch asynchronously. + /// + /// The ID of the message batch to delete. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is . + Task> DeleteMessageBatchAsync(string batchId); + + /// + /// Gets the results of a message batch asynchronously. + /// + /// The ID of the message batch to get the results for. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is where T is . + Task>> GetMessageBatchResultsAsync(string batchId); + + /// + /// Counts the tokens in a message asynchronously. + /// + /// The count message tokens request. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is . + Task> CountMessageTokensAsync(CountMessageTokensRequest request); + + /// + /// Lists the models asynchronously. + /// + /// The paging request to use for listing the models. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is where T is . + Task>> ListModelsAsync(PagingRequest? request = null); + + /// + /// Lists the models asynchronously + /// + /// The maximum number of models to return in each page. + /// An asynchronous enumerable that yields the response as an where T is where T is . + /// + IAsyncEnumerable>> ListAllModelsAsync(int limit = 20); + + /// + /// Gets a model by its ID asynchronously. + /// + /// The ID of the model to get. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is . + Task> GetModelAsync(string modelId); +} \ No newline at end of file diff --git a/src/AnthropicClient/Json/ContentDeltaConverter.cs b/src/AnthropicClient/Json/ContentDeltaConverter.cs index e384b8a..af2dbb8 100644 --- a/src/AnthropicClient/Json/ContentDeltaConverter.cs +++ b/src/AnthropicClient/Json/ContentDeltaConverter.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/AnthropicClient/Json/EventDataConverter.cs b/src/AnthropicClient/Json/EventDataConverter.cs index d11855b..31bb6fe 100644 --- a/src/AnthropicClient/Json/EventDataConverter.cs +++ b/src/AnthropicClient/Json/EventDataConverter.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/AnthropicClient/Json/JsonSerializationOptions.cs b/src/AnthropicClient/Json/JsonSerializationOptions.cs index 6e9dece..c1d5c40 100644 --- a/src/AnthropicClient/Json/JsonSerializationOptions.cs +++ b/src/AnthropicClient/Json/JsonSerializationOptions.cs @@ -17,6 +17,7 @@ static class JsonSerializationOptions new EventDataConverter(), new ContentDeltaConverter(), new JsonStringEnumConverter(), + new MessageBatchResultConverter(), }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; diff --git a/src/AnthropicClient/Json/MessageBatchResultConverter.cs b/src/AnthropicClient/Json/MessageBatchResultConverter.cs new file mode 100644 index 0000000..1df20b2 --- /dev/null +++ b/src/AnthropicClient/Json/MessageBatchResultConverter.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +using AnthropicClient.Models; + +namespace AnthropicClient.Json; + +class MessageBatchResultConverter : JsonConverter +{ + public override MessageBatchResult Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var jsonDocument = JsonDocument.ParseValue(ref reader); + var root = jsonDocument.RootElement; + var type = root.GetProperty("type").GetString(); + return type switch + { + MessageBatchResultType.Succeeded => JsonSerializer.Deserialize(root.GetRawText(), options)!, + MessageBatchResultType.Errored => JsonSerializer.Deserialize(root.GetRawText(), options)!, + MessageBatchResultType.Canceled => JsonSerializer.Deserialize(root.GetRawText(), options)!, + MessageBatchResultType.Expired => JsonSerializer.Deserialize(root.GetRawText(), options)!, + _ => throw new JsonException($"Unknown message batch result type: {type}") + }; + } + + public override void Write(Utf8JsonWriter writer, MessageBatchResult value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/CanceledMessageBatchResult.cs b/src/AnthropicClient/Models/CanceledMessageBatchResult.cs new file mode 100644 index 0000000..e2f02f6 --- /dev/null +++ b/src/AnthropicClient/Models/CanceledMessageBatchResult.cs @@ -0,0 +1,14 @@ +namespace AnthropicClient.Models; + +/// +/// Represents a message batch result that was cancelled. +/// +public class CanceledMessageBatchResult : MessageBatchResult +{ + /// + /// Initializes a new instance of the class. + /// + public CanceledMessageBatchResult() : base(MessageBatchResultType.Canceled) + { + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/ErroredMessageBatchResult.cs b/src/AnthropicClient/Models/ErroredMessageBatchResult.cs new file mode 100644 index 0000000..467dc7e --- /dev/null +++ b/src/AnthropicClient/Models/ErroredMessageBatchResult.cs @@ -0,0 +1,19 @@ +namespace AnthropicClient.Models; + +/// +/// Represents a message batch result that contains an error response. +/// +public class ErroredMessageBatchResult : MessageBatchResult +{ + /// + /// Gets the error of the message batch result. + /// + public AnthropicError Error { get; init; } = new AnthropicError(); + + /// + /// Initializes a new instance of the class. + /// + public ErroredMessageBatchResult() : base(MessageBatchResultType.Errored) + { + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/ExpiredMessageBatchResult.cs b/src/AnthropicClient/Models/ExpiredMessageBatchResult.cs new file mode 100644 index 0000000..11f9f97 --- /dev/null +++ b/src/AnthropicClient/Models/ExpiredMessageBatchResult.cs @@ -0,0 +1,14 @@ +namespace AnthropicClient.Models; + +/// +/// Represents a message batch result that has expired. +/// +public class ExpiredMessageBatchResult : MessageBatchResult +{ + /// + /// Initializes a new instance of the class. + /// + public ExpiredMessageBatchResult() : base(MessageBatchResultType.Expired) + { + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageBatchDeleteResponse.cs b/src/AnthropicClient/Models/MessageBatchDeleteResponse.cs new file mode 100644 index 0000000..877c808 --- /dev/null +++ b/src/AnthropicClient/Models/MessageBatchDeleteResponse.cs @@ -0,0 +1,17 @@ +namespace AnthropicClient.Models; + +/// +/// Represents a message batch delete response. +/// +public class MessageBatchDeleteResponse +{ + /// + /// Gets the ID of the message batch that was deleted. + /// + public string Id { get; init; } = string.Empty; + + /// + /// Gets the type of the message batch response. + /// + public string Type { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageBatchRequest.cs b/src/AnthropicClient/Models/MessageBatchRequest.cs new file mode 100644 index 0000000..0af5514 --- /dev/null +++ b/src/AnthropicClient/Models/MessageBatchRequest.cs @@ -0,0 +1,28 @@ +namespace AnthropicClient.Models; + +/// +/// Represents a request to create a batch of messages. +/// +public class MessageBatchRequest +{ + /// + /// Gets the requests to create messages. + /// + public List Requests { get; init; } = []; + + /// + /// Initializes a new instance of the class. + /// + /// The requests to create messages. + /// Thrown when is empty. + /// An instance of the class. + public MessageBatchRequest(List requests) + { + if (requests.Count == 0) + { + throw new ArgumentException($"{nameof(requests)} must not be empty.", nameof(requests)); + } + + Requests = requests; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageBatchRequestItem.cs b/src/AnthropicClient/Models/MessageBatchRequestItem.cs new file mode 100644 index 0000000..aec4aee --- /dev/null +++ b/src/AnthropicClient/Models/MessageBatchRequestItem.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +using AnthropicClient.Utils; + +namespace AnthropicClient.Models; + +/// +/// Represents an item in a batch of messages. +/// +public class MessageBatchRequestItem +{ + /// + /// Gets the custom identifier for the message. + /// + [JsonPropertyName("custom_id")] + public string CustomId { get; init; } + + /// + /// Gets the message request parameters. + /// + public MessageRequest Params { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The custom identifier for the message. + /// The message request parameters. + /// Thrown when is null or whitespace. + /// Thrown when is null. + /// An instance of the class. + public MessageBatchRequestItem(string customId, MessageRequest messageRequest) + { + ArgumentValidator.ThrowIfNullOrWhitespace(customId, nameof(customId)); + ArgumentValidator.ThrowIfNull(messageRequest, nameof(messageRequest)); + + CustomId = customId; + Params = messageRequest; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageBatchResponse.cs b/src/AnthropicClient/Models/MessageBatchResponse.cs new file mode 100644 index 0000000..2df3b43 --- /dev/null +++ b/src/AnthropicClient/Models/MessageBatchResponse.cs @@ -0,0 +1,98 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +/// +/// Represents a response to a batch of messages. +/// +public class MessageBatchResponse +{ + /// + /// Gets the identifier of the batch. + /// + public string Id { get; init; } = string.Empty; + + /// + /// Gets the type of the batch. + /// + public string Type { get; init; } = string.Empty; + + /// + /// Gets the processing status of the batch. + /// + [JsonPropertyName("processing_status")] + public string ProcessingStatus { get; init; } = string.Empty; + + /// + /// Gets the counts of requests in the batch. + /// + [JsonPropertyName("request_counts")] + public MessageBatchRequestCounts RequestCounts { get; init; } = new(); + + /// + /// Gets the date and time when the batch ended. + /// + [JsonPropertyName("ended_at")] + public DateTimeOffset? EndedAt { get; init; } = DateTimeOffset.MinValue; + + /// + /// Gets the date and time when the batch was created. + /// + [JsonPropertyName("created_at")] + public DateTimeOffset CreatedAt { get; init; } + + /// + /// Gets the date and time when the batch expires. + /// + [JsonPropertyName("expires_at")] + public DateTimeOffset ExpiresAt { get; init; } + + /// + /// Gets the date and time when the batch was archived. + /// + [JsonPropertyName("archived_at")] + public DateTimeOffset? ArchivedAt { get; init; } = DateTimeOffset.MinValue; + + /// + /// Gets the date and time when the batch cancellation was initiated. + /// + [JsonPropertyName("cancel_initiated_at")] + public DateTimeOffset? CancelInitiatedAt { get; init; } = DateTimeOffset.MinValue; + + /// + /// Gets the URL to the results of the batch. + /// + [JsonPropertyName("results_url")] + public string? ResultsUrl { get; init; } = string.Empty; +} + +/// +/// Represents the counts of requests in a batch of messages. +/// +public class MessageBatchRequestCounts +{ + /// + /// Gets the number of requests in the batch that are processing. + /// + public int Processing { get; init; } + + /// + /// Gets the number of requests in the batch that succeeded. + /// + public int Succeeded { get; init; } + + /// + /// Gets the number of requests in the batch that errored. + /// + public int Errored { get; init; } + + /// + /// Gets the number of requests in the batch that were cancelled. + /// + public int Canceled { get; init; } + + /// + /// Gets the number of requests in the batch that expired. + /// + public int Expired { get; init; } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageBatchResult.cs b/src/AnthropicClient/Models/MessageBatchResult.cs new file mode 100644 index 0000000..6531475 --- /dev/null +++ b/src/AnthropicClient/Models/MessageBatchResult.cs @@ -0,0 +1,26 @@ +using AnthropicClient.Utils; + +namespace AnthropicClient.Models; + +/// +/// Represents a message batch result. +/// +public abstract class MessageBatchResult +{ + /// + /// Gets the type of the message batch result. + /// + public string Type { get; init; } = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + /// The type of the message batch result. + /// An instance of the class. + public MessageBatchResult(string type) + { + ArgumentValidator.ThrowIfNullOrWhitespace(type, nameof(type)); + + Type = type; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageBatchResultItem.cs b/src/AnthropicClient/Models/MessageBatchResultItem.cs new file mode 100644 index 0000000..5613619 --- /dev/null +++ b/src/AnthropicClient/Models/MessageBatchResultItem.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +/// +/// Represents a message batch result item. +/// +public class MessageBatchResultItem +{ + /// + /// Gets the custom ID of the message batch result item. + /// + [JsonPropertyName("custom_id")] + public string CustomId { get; init; } = string.Empty; + + /// + /// Gets the result of the message batch result item. + /// + public MessageBatchResult Result { get; init; } = default!; +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageBatchResultType.cs b/src/AnthropicClient/Models/MessageBatchResultType.cs new file mode 100644 index 0000000..6f14964 --- /dev/null +++ b/src/AnthropicClient/Models/MessageBatchResultType.cs @@ -0,0 +1,27 @@ +namespace AnthropicClient.Models; + +/// +/// Represents the types of message batch results. +/// +public static class MessageBatchResultType +{ + /// + /// Represents a succeeded message batch result. + /// + public const string Succeeded = "succeeded"; + + /// + /// Represents an errored message batch result. + /// + public const string Errored = "errored"; + + /// + /// Represents a canceled message batch result. + /// + public const string Canceled = "canceled"; + + /// + /// Represents an expired message batch result. + /// + public const string Expired = "expired"; +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageBatchStatus.cs b/src/AnthropicClient/Models/MessageBatchStatus.cs new file mode 100644 index 0000000..6730674 --- /dev/null +++ b/src/AnthropicClient/Models/MessageBatchStatus.cs @@ -0,0 +1,22 @@ +namespace AnthropicClient.Models; + +/// +/// Represents the status of a message batch. +/// +public static class MessageBatchStatus +{ + /// + /// The status of a message batch that is being canceled. + /// + public const string Canceling = "canceling"; + + /// + /// The status of a message batch that is in progress. + /// + public const string InProgress = "in_progress"; + + /// + /// The status of a message batch that has ended. + /// + public const string Ended = "ended"; +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/SucceededMessageBatchResult.cs b/src/AnthropicClient/Models/SucceededMessageBatchResult.cs new file mode 100644 index 0000000..de8bcea --- /dev/null +++ b/src/AnthropicClient/Models/SucceededMessageBatchResult.cs @@ -0,0 +1,20 @@ +namespace AnthropicClient.Models; + +/// +/// Represents a message batch result that contains a message response. +/// +public class SucceededMessageBatchResult : MessageBatchResult +{ + /// + /// Gets the message of the message batch result. + /// + public MessageResponse Message { get; init; } = new MessageResponse(); + + /// + /// Initializes a new instance of the class. + /// + /// An instance of the class. + public SucceededMessageBatchResult() : base(MessageBatchResultType.Succeeded) + { + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs index 47ccadd..8fa81f6 100644 --- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs @@ -1,10 +1,9 @@ +using AnthropicClient.Tests.Files; + namespace AnthropicClient.Tests.EndToEnd; -public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture) +public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture) { - private string GetTestFilePath(string fileName) => - Path.Combine(Directory.GetCurrentDirectory(), "Files", fileName); - [Fact] public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse() { @@ -63,7 +62,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf [Fact] public async Task CreateMessageAsync_WhenImageIsSent_ItShouldReturnResponse() { - var imagePath = GetTestFilePath("elephant.jpg"); + var imagePath = TestFileHelper.GetTestFilePath("elephant.jpg"); var mediaType = "image/jpeg"; var bytes = await File.ReadAllBytesAsync(imagePath); var base64Data = Convert.ToBase64String(bytes); @@ -102,7 +101,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf { var client = CreateClient(new HttpClient()); - var storyPath = GetTestFilePath("story.txt"); + var storyPath = TestFileHelper.GetTestFilePath("story.txt"); var storyText = await File.ReadAllTextAsync(storyPath); var request = new MessageRequest( @@ -141,7 +140,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf { var client = CreateClient(new HttpClient()); - var storyPath = GetTestFilePath("story.txt"); + var storyPath = TestFileHelper.GetTestFilePath("story.txt"); var storyText = await File.ReadAllTextAsync(storyPath); var request = new MessageRequest( @@ -217,7 +216,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf [Fact] public async Task CreateMessageAsync_WhenProvidedWithPDF_ItShouldReturnResponse() { - var pdfPath = GetTestFilePath("addendum.pdf"); + var pdfPath = TestFileHelper.GetTestFilePath("addendum.pdf"); var bytes = await File.ReadAllBytesAsync(pdfPath); var base64Data = Convert.ToBase64String(bytes); @@ -253,7 +252,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf [Fact] public async Task CreateMessageAsync_WhenProvidedWithPDFWithCacheControl_ItShouldUseCache() { - var pdfPath = GetTestFilePath("addendum.pdf"); + var pdfPath = TestFileHelper.GetTestFilePath("addendum.pdf"); var bytes = await File.ReadAllBytesAsync(pdfPath); var base64Data = Convert.ToBase64String(bytes); @@ -341,4 +340,121 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf result.Value.Should().BeOfType(); result.Value.Id.Should().Be(AnthropicModels.Claude3Haiku); } + + [Fact] + public async Task CreateMessageBatchAsync_WhenCalled_ItShouldReturnResponse() + { + var request = new MessageBatchRequest([ + new( + Guid.NewGuid().ToString(), + new( + model: AnthropicModels.Claude3Haiku, + messages: [new(MessageRole.User, [new TextContent("Hello!")])] + ) + ), + ]); + + var result = await _client.CreateMessageBatchAsync(request); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType(); + result.Value.Id.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task GetMessageBatchAsync_WhenCalled_ItShouldReturnResponse() + { + var request = new MessageBatchRequest([ + new( + Guid.NewGuid().ToString(), + new( + model: AnthropicModels.Claude3Haiku, + messages: [new(MessageRole.User, [new TextContent("Hello!")])] + ) + ), + ]); + + var createResult = await _client.CreateMessageBatchAsync(request); + var getResult = await _client.GetMessageBatchAsync(createResult.Value.Id); + + getResult.IsSuccess.Should().BeTrue(); + getResult.Value.Should().BeOfType(); + getResult.Value.Id.Should().Be(createResult.Value.Id); + } + + [Fact] + public async Task ListMessageBatchesAsync_WhenCalled_ItShouldReturnResponse() + { + var request = new MessageBatchRequest([ + new( + Guid.NewGuid().ToString(), + new( + model: AnthropicModels.Claude3Haiku, + messages: [new(MessageRole.User, [new TextContent("Hello!")])] + ) + ), + ]); + + var createResult = await _client.CreateMessageBatchAsync(request); + var result = await _client.ListMessageBatchesAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType>(); + result.Value.Data.Should().HaveCountGreaterThan(0); + result.Value.Data.Should().ContainSingle(b => b.Id == createResult.Value.Id); + } + + [Fact] + public async Task ListAllMessageBatchesAsync_WhenCalled_ItShouldReturnResponse() + { + var createRequest = (string id) => new MessageBatchRequest([ + new( + id, + new( + model: AnthropicModels.Claude3Haiku, + messages: [new(MessageRole.User, [new TextContent("Hello!")])] + ) + ), + ]); + + var requestNumberOne = createRequest(Guid.NewGuid().ToString()); + var requestNumberTwo = createRequest(Guid.NewGuid().ToString()); + + var createResultOne = await _client.CreateMessageBatchAsync(requestNumberOne); + var createResultTwo = await _client.CreateMessageBatchAsync(requestNumberTwo); + + var responses = await _client.ListAllMessageBatchesAsync(limit: 1).ToListAsync(); + + responses.Should().HaveCountGreaterThan(2); + + var batches = responses + .Where(r => r.IsSuccess) + .Select(r => r.Value) + .SelectMany(r => r.Data); + + batches.Should().ContainSingle(b => b.Id == createResultOne.Value.Id); + batches.Should().ContainSingle(b => b.Id == createResultTwo.Value.Id); + } + + [Fact] + public async Task CancelMessageBatchAsync_WhenCalled_ItShouldReturnResponse() + { + var request = new MessageBatchRequest([ + new( + Guid.NewGuid().ToString(), + new( + model: AnthropicModels.Claude3Haiku, + messages: [new(MessageRole.User, [new TextContent("Hello!")])] + ) + ), + ]); + + var createResult = await _client.CreateMessageBatchAsync(request); + var result = await _client.CancelMessageBatchAsync(createResult.Value.Id); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType(); + result.Value.Id.Should().Be(createResult.Value.Id); + result.Value.ProcessingStatus.Should().Be(MessageBatchStatus.Canceling); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Files/TestFileHelper.cs b/tests/AnthropicClient.Tests/Files/TestFileHelper.cs new file mode 100644 index 0000000..efc9ca1 --- /dev/null +++ b/tests/AnthropicClient.Tests/Files/TestFileHelper.cs @@ -0,0 +1,7 @@ +namespace AnthropicClient.Tests.Files; + +static class TestFileHelper +{ + public static string GetTestFilePath(string fileName) => + Path.Combine(Directory.GetCurrentDirectory(), "Files", fileName); +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Files/batch_results.jsonl b/tests/AnthropicClient.Tests/Files/batch_results.jsonl new file mode 100644 index 0000000..3647eca --- /dev/null +++ b/tests/AnthropicClient.Tests/Files/batch_results.jsonl @@ -0,0 +1,5 @@ +{"custom_id":"my-fifth-request","result":{"type":"errored","error":{"type":"error","error":{"type":"not_found_error","message":"The requested resource could not be found."}}}} +{"custom_id":"my-fourth-request","result":{"type":"expired"}} +{"custom_id":"my-third-request","result":{"type":"canceled"}} +{"custom_id":"my-second-request","result":{"type":"succeeded","message":{"id":"msg_014VwiXbi91y3JMjcpyGBHX5","type":"message","role":"assistant","model":"claude-3-5-sonnet-20240620","content":[{"type":"text","text":"Hello again! It's nice to see you. How can I assist you today? Is there anything specific you'd like to chat about or any questions you have?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":11,"output_tokens":36}}}} +{"custom_id":"my-first-request","result":{"type":"succeeded","message":{"id":"msg_01FqfsLoHwgeFbguDgpz48m7","type":"message","role":"assistant","model":"claude-3-5-sonnet-20240620","content":[{"type":"text","text":"Hello! How can I assist you today? Feel free to ask me any questions or let me know if there's anything you'd like to chat about."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":34}}}} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs index eaef353..6cab022 100644 --- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs @@ -1,3 +1,5 @@ +using AnthropicClient.Tests.Files; + namespace AnthropicClient.Tests.Integration; public class AnthropicApiClientTests : IntegrationTest @@ -32,6 +34,51 @@ public class AnthropicApiClientTests : IntegrationTest actualErrorType.Should().Be(errorType); } + [Fact] + public async Task CreateMessageAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() + { + _mockHttpMessageHandler + .WhenCreateMessageRequest() + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"null" + ); + + var request = new MessageRequest( + model: AnthropicModels.Claude3Haiku, + messages: [new(MessageRole.User, [new TextContent("Hello!")])] + ); + + var result = await Client.CreateMessageAsync(request); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task CreateMessageAsync_WhenCalledRequestSucceedsAndCanNotDeserializeResponse_ItShouldReturnEmptyResponse() + { + _mockHttpMessageHandler + .WhenCreateMessageRequest() + .Respond( + HttpStatusCode.OK, + "application/json", + @"null" + ); + + var request = new MessageRequest( + model: AnthropicModels.Claude3Haiku, + messages: [new(MessageRole.User, [new TextContent("Hello!")])] + ); + + var result = await Client.CreateMessageAsync(request); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeEquivalentTo(new MessageResponse()); + } + [Fact] public async Task CreateMessageAsync_WhenCalledAndMessageCreatedWithTextContent_ItShouldReturnMessage() { @@ -360,6 +407,34 @@ public class AnthropicApiClientTests : IntegrationTest )); } + [Fact] + public async Task CreateMessageAsync_WhenCalledMessageIsStreamedRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownErrorEvent() + { + _mockHttpMessageHandler + .WhenCreateStreamMessageRequest() + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"null" + ); + + var request = new StreamMessageRequest( + model: AnthropicModels.Claude35Sonnet, + messages: [ + new(MessageRole.User, [new TextContent("Hello!")]), + new(MessageRole.User, [new TextContent("Hello!")]) + ] + ); + + var result = Client.CreateMessageAsync(request); + var events = await result.ToListAsync(); + + events.Should().HaveCount(1); + events[0].Type.Should().Be(EventType.Error); + events[0].Data.Should().BeOfType(); + events[0].Data.Should().BeEquivalentTo(new ErrorEventData(new ApiError())); + } + [Fact] public async Task CreateMessageAsync_WhenCalledAndMessageCreatedWithDocumentContent_ItShouldReturnMessage() { @@ -486,7 +561,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.BadRequest, "application/json", - @"{}" + @"null" ); var request = new CountMessageTokensRequest( @@ -504,6 +579,31 @@ public class AnthropicApiClientTests : IntegrationTest result.Error.Error.Should().BeOfType(); } + [Fact] + public async Task CountMessageTokensAsync_WhenCalledAndResponseCanNotBeDeserialized_ItShouldReturnEmptyResponse() + { + _mockHttpMessageHandler + .WhenCountMessageTokensRequest() + .Respond( + HttpStatusCode.OK, + "application/json", + @"null" + ); + + var request = new CountMessageTokensRequest( + model: AnthropicModels.Claude35Sonnet, + messages: [ + new(MessageRole.User, [new TextContent("Hello!")]), + ] + ); + + var result = await Client.CountMessageTokensAsync(request); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType(); + result.Value.InputTokens.Should().Be(0); + } + [Fact] public async Task ListModelsAsync_WhenCalledWithPagingRequestUsingDefaultValues_ItShouldReturnListOfModels() { @@ -653,7 +753,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.BadRequest, "application/json", - @"{}" + @"null" ); var result = await Client.ListModelsAsync(); @@ -663,6 +763,27 @@ public class AnthropicApiClientTests : IntegrationTest result.Error.Error.Should().BeOfType(); } + [Fact] + public async Task ListModelAsync_WhenCalledRequestSucceedsAndCanNotDeserializeResponse_ItShouldReturnEmptyPage() + { + _mockHttpMessageHandler + .WhenListModelsRequest() + .Respond( + HttpStatusCode.OK, + "application/json", + @"null" + ); + + var result = await Client.ListModelsAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType>(); + result.Value.HasMore.Should().BeFalse(); + result.Value.FirstId.Should().BeEmpty(); + result.Value.LastId.Should().BeEmpty(); + result.Value.Data.Should().BeEmpty(); + } + [Fact] public async Task ListAllModelsAsync_WhenCalled_ItShouldReturnAllModels() { @@ -800,7 +921,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.BadRequest, "application/json", - @"{}" + @"null" ); var responses = Client.ListAllModelsAsync(); @@ -886,6 +1007,35 @@ public class AnthropicApiClientTests : IntegrationTest count.Should().Be(2); } + [Fact] + public async Task ListAllModelsAsync_WhenFirstPageSucceedsButResponseCanNotBeDeserialized_ItShouldReturnEmptyPage() + { + _mockHttpMessageHandler + .WhenListModelsRequest() + .WithExactQueryString(new Dictionary + { + { "limit", "20" }, + }) + .Respond( + HttpStatusCode.OK, + "application/json", + @"null" + ); + + var responses = Client.ListAllModelsAsync(); + var count = 0; + + await foreach (var page in responses) + { + count++; + page.IsSuccess.Should().BeTrue(); + page.Value.Should().BeOfType>(); + page.Value.Data.Should().BeEmpty(); + } + + count.Should().Be(1); + } + [Fact] public async Task GetModelAsync_WhenCalled_ItShouldReturnModel() { @@ -950,7 +1100,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.BadRequest, "application/json", - @"{}" + @"null" ); var result = await Client.GetModelAsync(modelId); @@ -970,7 +1120,7 @@ public class AnthropicApiClientTests : IntegrationTest .Respond( HttpStatusCode.OK, "application/json", - @"{}" + @"null" ); var result = await Client.GetModelAsync(modelId); @@ -978,4 +1128,804 @@ public class AnthropicApiClientTests : IntegrationTest result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); } + + [Fact] + public async Task CreateMessageBatchAsync_WhenCalled_ItShouldReturnBatch() + { + _mockHttpMessageHandler + .WhenCreateMessageBatchRequest() + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", + ""type"": ""message_batch"", + ""processing_status"": ""in_progress"", + ""request_counts"": { + ""processing"": 100, + ""succeeded"": 50, + ""errored"": 30, + ""canceled"": 10, + ""expired"": 10 + }, + ""ended_at"": ""2024-08-20T18:37:24.100435Z"", + ""created_at"": ""2024-08-20T18:37:24.100435Z"", + ""expires_at"": ""2024-08-20T18:37:24.100435Z"", + ""archived_at"": ""2024-08-20T18:37:24.100435Z"", + ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", + ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" + }" + ); + + var request = new MessageBatchRequest([new("custom_id", new())]); + + var result = await Client.CreateMessageBatchAsync(request); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType(); + result.Value.Id.Should().Be("msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"); + result.Value.Type.Should().Be("message_batch"); + result.Value.ProcessingStatus.Should().Be("in_progress"); + result.Value.RequestCounts.Should().BeEquivalentTo(new MessageBatchRequestCounts + { + Processing = 100, + Succeeded = 50, + Errored = 30, + Canceled = 10, + Expired = 10 + }); + + result.Value.EndedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); + result.Value.CreatedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); + result.Value.ExpiresAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); + result.Value.ArchivedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); + result.Value.CancelInitiatedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); + result.Value.ResultsUrl.Should().Be("https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"); + } + + [Fact] + public async Task CreateMessageBatchAsync_WhenCalledAndErrorReturned_ItShouldHandleError() + { + _mockHttpMessageHandler + .WhenCreateMessageBatchRequest() + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"{ + ""type"": ""error"", + ""error"": { + ""type"": ""invalid_request_error"", + ""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row"" + } + }" + ); + + var request = new MessageBatchRequest([new("custom_id", new())]); + + var result = await Client.CreateMessageBatchAsync(request); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task CreateMessageBatchAsync_WhenCalledRequestFailsAndErrorCanNotBeDeserialized_ItShouldReturnUnknownError() + { + _mockHttpMessageHandler + .WhenCreateMessageBatchRequest() + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"null" + ); + + var request = new MessageBatchRequest([new("custom_id", new())]); + + var result = await Client.CreateMessageBatchAsync(request); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task CreateMessageBatchAsync_WhenCalledRequestSucceedsAndCanNotDeserializeResponse_ItShouldReturnEmptyResponse() + { + _mockHttpMessageHandler + .WhenCreateMessageBatchRequest() + .Respond( + HttpStatusCode.OK, + "application/json", + @"null" + ); + + var request = new MessageBatchRequest([new("custom_id", new())]); + + var result = await Client.CreateMessageBatchAsync(request); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType(); + result.Value.Should().BeEquivalentTo(new MessageBatchResponse()); + } + + [Fact] + public async Task GetMessageBatchAsync_WhenCalled_ItShouldReturnBatch() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchRequest(batchId) + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", + ""type"": ""message_batch"", + ""processing_status"": ""in_progress"", + ""request_counts"": { + ""processing"": 100, + ""succeeded"": 50, + ""errored"": 30, + ""canceled"": 10, + ""expired"": 10 + }, + ""ended_at"": ""2024-08-20T18:37:24.100435Z"", + ""created_at"": ""2024-08-20T18:37:24.100435Z"", + ""expires_at"": ""2024-08-20T18:37:24.100435Z"", + ""archived_at"": ""2024-08-20T18:37:24.100435Z"", + ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", + ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" + }" + ); + + var result = await Client.GetMessageBatchAsync(batchId); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeEquivalentTo(new MessageBatchResponse() + { + Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", + Type = "message_batch", + ProcessingStatus = "in_progress", + RequestCounts = new MessageBatchRequestCounts + { + Processing = 100, + Succeeded = 50, + Errored = 30, + Canceled = 10, + Expired = 10 + }, + EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" + }); + } + + [Fact] + public async Task GetMessageBatchAsync_WhenCalledAndErrorReturned_ItShouldHandleError() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchRequest(batchId) + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"{ + ""type"": ""error"", + ""error"": { + ""type"": ""invalid_request_error"", + ""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row"" + } + }" + ); + + var result = await Client.GetMessageBatchAsync(batchId); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task GetMessageBatchAsync_WhenCalledRequestFailsAndErrorCanNotBeDeserialized_ItShouldReturnUnknownError() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchRequest(batchId) + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"null" + ); + + var result = await Client.GetMessageBatchAsync(batchId); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task GetMessageBatchAsync_WhenCalledAndCanNotDeserializeResponse_ItShouldReturnEmptyResponse() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchRequest(batchId) + .Respond( + HttpStatusCode.OK, + "application/json", + @"null" + ); + + var result = await Client.GetMessageBatchAsync(batchId); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType(); + } + + [Fact] + public async Task GetMessageBatchResultsAsync_WhenCalledAndSuccessful_ItShouldReturnAllBatchResults() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + var batchResultsPath = TestFileHelper.GetTestFilePath("batch_results.jsonl"); + var batchResultsText = await File.ReadAllTextAsync(batchResultsPath); + var batchResults = batchResultsText.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + var expectedResults = batchResults.Select(Deserialize); + + _mockHttpMessageHandler + .WhenGetMessageBatchResultsRequest(batchId) + .Respond( + HttpStatusCode.OK, + "application/x-jsonl", + batchResultsText + ); + + var result = await Client.GetMessageBatchResultsAsync(batchId); + + result.IsSuccess.Should().BeTrue(); + + var actualResults = await result.Value.ToListAsync(); + + actualResults.Should().BeEquivalentTo(expectedResults); + } + + [Fact] + public async Task GetMessageBatchResultsAsync_WhenCalledSuccessfulAndResultCanNotBeDeserialized_ItShouldReturnEmptyResults() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + var expectedResults = new List() + { + new(), + }; + + _mockHttpMessageHandler + .WhenGetMessageBatchResultsRequest(batchId) + .Respond( + HttpStatusCode.OK, + "application/x-jsonl", + "null" + ); + + var result = await Client.GetMessageBatchResultsAsync(batchId); + + result.IsSuccess.Should().BeTrue(); + + var actualResults = await result.Value.ToListAsync(); + + actualResults.Should().BeEquivalentTo(expectedResults); + } + + [Fact] + public async Task GetMessageBatchResultsAsync_WhenCalledAndRequestFails_ItShouldReturnError() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchResultsRequest(batchId) + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"{ + ""type"": ""error"", + ""error"": { + ""type"": ""invalid_request_error"", + ""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row"" + } + }" + ); + + var result = await Client.GetMessageBatchResultsAsync(batchId); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task GetMessageBatchResultsAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchResultsRequest(batchId) + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"null" + ); + + var result = await Client.GetMessageBatchResultsAsync(batchId); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task ListMessageBatchesAsync_WhenCalledAndSuccessful_ItShouldReturnPageOfBatches() + { + _mockHttpMessageHandler + .WhenListMessageBatchesRequest() + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""data"": [ + { + ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", + ""type"": ""message_batch"", + ""processing_status"": ""in_progress"", + ""request_counts"": { + ""processing"": 100, + ""succeeded"": 50, + ""errored"": 30, + ""canceled"": 10, + ""expired"": 10 + }, + ""ended_at"": ""2024-08-20T18:37:24.100435Z"", + ""created_at"": ""2024-08-20T18:37:24.100435Z"", + ""expires_at"": ""2024-08-20T18:37:24.100435Z"", + ""archived_at"": ""2024-08-20T18:37:24.100435Z"", + ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", + ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" + } + ], + ""has_more"": true, + ""first_id"": ""1"", + ""last_id"": ""1"" + }" + ); + + var result = await Client.ListMessageBatchesAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType>(); + result.Value.HasMore.Should().BeTrue(); + result.Value.FirstId.Should().Be("1"); + result.Value.LastId.Should().Be("1"); + result.Value.Data.Should().BeEquivalentTo(new MessageBatchResponse[] + { + new() + { + Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", + Type = "message_batch", + ProcessingStatus = "in_progress", + RequestCounts = new MessageBatchRequestCounts + { + Processing = 100, + Succeeded = 50, + Errored = 30, + Canceled = 10, + Expired = 10 + }, + EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" + } + }); + } + + [Fact] + public async Task ListMessageBatchesAsync_WhenCalledWithPagingRequestAndSuccessful_ItShouldReturnPageOfBatches() + { + var pagingRequest = new PagingRequest(afterId: "next_id", limit: 10); + + _mockHttpMessageHandler + .WhenListMessageBatchesRequest() + .WithQueryString(new Dictionary + { + { "after_id", pagingRequest.AfterId }, + { "limit", pagingRequest.Limit.ToString() }, + }) + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""data"": [ + { + ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", + ""type"": ""message_batch"", + ""processing_status"": ""in_progress"", + ""request_counts"": { + ""processing"": 100, + ""succeeded"": 50, + ""errored"": 30, + ""canceled"": 10, + ""expired"": 10 + }, + ""ended_at"": ""2024-08-20T18:37:24.100435Z"", + ""created_at"": ""2024-08-20T18:37:24.100435Z"", + ""expires_at"": ""2024-08-20T18:37:24.100435Z"", + ""archived_at"": ""2024-08-20T18:37:24.100435Z"", + ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", + ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" + } + ], + ""has_more"": true, + ""first_id"": ""1"", + ""last_id"": ""1"" + }" + ); + + var result = await Client.ListMessageBatchesAsync(pagingRequest); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType>(); + result.Value.HasMore.Should().BeTrue(); + result.Value.FirstId.Should().Be("1"); + result.Value.LastId.Should().Be("1"); + result.Value.Data.Should().BeEquivalentTo(new MessageBatchResponse[] + { + new() + { + Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", + Type = "message_batch", + ProcessingStatus = "in_progress", + RequestCounts = new MessageBatchRequestCounts + { + Processing = 100, + Succeeded = 50, + Errored = 30, + Canceled = 10, + Expired = 10 + }, + EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" + } + }); + } + + [Fact] + public async Task ListMessageBatchesAsync_WhenCalledAndNoBatchesReturned_ItShouldReturnEmptyList() + { + _mockHttpMessageHandler + .WhenListMessageBatchesRequest() + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""data"": [], + ""has_more"": false, + ""first_id"": null, + ""last_id"": null + }" + ); + + var result = await Client.ListMessageBatchesAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType>(); + result.Value.HasMore.Should().BeFalse(); + result.Value.FirstId.Should().BeNull(); + result.Value.LastId.Should().BeNull(); + result.Value.Data.Should().BeEmpty(); + } + + [Fact] + public async Task ListMessageBatchesAsync_WhenCalledAndErrorReturned_ItShouldHandleError() + { + _mockHttpMessageHandler + .WhenListMessageBatchesRequest() + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"{ + ""type"": ""error"", + ""error"": { + ""type"": ""invalid_request_error"", + ""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row"" + } + }" + ); + + var result = await Client.ListMessageBatchesAsync(); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task ListMessageBatchesAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() + { + _mockHttpMessageHandler + .WhenListMessageBatchesRequest() + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"null" + ); + + var result = await Client.ListMessageBatchesAsync(); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task ListMessageBatchesAsync_WhenCalledRequestSucceedsAndCanNotDeserializeResponse_ItShouldReturnEmptyPage() + { + _mockHttpMessageHandler + .WhenListMessageBatchesRequest() + .Respond( + HttpStatusCode.OK, + "application/json", + @"null" + ); + + var result = await Client.ListMessageBatchesAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType>(); + result.Value.HasMore.Should().BeFalse(); + result.Value.FirstId.Should().BeEmpty(); + result.Value.LastId.Should().BeEmpty(); + result.Value.Data.Should().BeEmpty(); + } + + [Fact] + public async Task ListAllMessageBatchesAsync_WhenCalled_ItShouldReturnAllBatches() + { + _mockHttpMessageHandler + .WhenListMessageBatchesRequest() + .WithExactQueryString(new Dictionary() + { + { "limit", "20" }, + }) + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""data"": [ + { + ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", + ""type"": ""message_batch"", + ""processing_status"": ""in_progress"", + ""request_counts"": { + ""processing"": 100, + ""succeeded"": 50, + ""errored"": 30, + ""canceled"": 10, + ""expired"": 10 + }, + ""ended_at"": ""2024-08-20T18:37:24.100435Z"", + ""created_at"": ""2024-08-20T18:37:24.100435Z"", + ""expires_at"": ""2024-08-20T18:37:24.100435Z"", + ""archived_at"": ""2024-08-20T18:37:24.100435Z"", + ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", + ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" + } + ], + ""has_more"": true, + ""first_id"": ""1"", + ""last_id"": ""1"" + }" + ); + + _mockHttpMessageHandler + .WhenListMessageBatchesRequest() + .WithExactQueryString(new Dictionary() + { + { "after_id", "1" }, + { "limit", "20" }, + }) + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""data"": [ + { + ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", + ""type"": ""message_batch"", + ""processing_status"": ""in_progress"", + ""request_counts"": { + ""processing"": 100, + ""succeeded"": 50, + ""errored"": 30, + ""canceled"": 10, + ""expired"": 10 + }, + ""ended_at"": ""2024-08-20T18:37:24.100435Z"", + ""created_at"": ""2024-08-20T18:37:24.100435Z"", + ""expires_at"": ""2024-08-20T18:37:24.100435Z"", + ""archived_at"": ""2024-08-20T18:37:24.100435Z"", + ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", + ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" + } + ], + ""has_more"": false, + ""first_id"": ""2"", + ""last_id"": ""2"" + }" + ); + + var pageResponses = Client.ListAllMessageBatchesAsync(); + var collectedPages = new List>(); + + await foreach (var response in pageResponses) + { + response.IsSuccess.Should().BeTrue(); + response.Value.Should().BeOfType>(); + collectedPages.Add(response.Value); + } + + var expectedMessageBatchResponse = new MessageBatchResponse() + { + Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", + Type = "message_batch", + ProcessingStatus = "in_progress", + RequestCounts = new MessageBatchRequestCounts + { + Processing = 100, + Succeeded = 50, + Errored = 30, + Canceled = 10, + Expired = 10 + }, + EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" + }; + + collectedPages.Should().HaveCount(2); + collectedPages.Should().BeEquivalentTo(new List>() + { + new() + { + Data = [expectedMessageBatchResponse], + FirstId = "1", + LastId = "1", + HasMore = true + }, + new() + { + Data = [expectedMessageBatchResponse], + FirstId = "2", + LastId = "2", + HasMore = false + } + }); + } + + [Fact] + public async Task CancelMessageBatchAsync_WhenCalled_ItShouldReturnBatch() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenCancelMessageBatchRequest(batchId) + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", + ""type"": ""message_batch"", + ""processing_status"": ""in_progress"", + ""request_counts"": { + ""processing"": 100, + ""succeeded"": 50, + ""errored"": 30, + ""canceled"": 10, + ""expired"": 10 + }, + ""ended_at"": ""2024-08-20T18:37:24.100435Z"", + ""created_at"": ""2024-08-20T18:37:24.100435Z"", + ""expires_at"": ""2024-08-20T18:37:24.100435Z"", + ""archived_at"": ""2024-08-20T18:37:24.100435Z"", + ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", + ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" + }" + ); + + var result = await Client.CancelMessageBatchAsync(batchId); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeEquivalentTo(new MessageBatchResponse() + { + Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", + Type = "message_batch", + ProcessingStatus = "in_progress", + RequestCounts = new MessageBatchRequestCounts + { + Processing = 100, + Succeeded = 50, + Errored = 30, + Canceled = 10, + Expired = 10 + }, + EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" + }); + } + + [Fact] + public async Task CancelMessageBatchAsync_WhenCalledAndFails_ItShouldReturnError() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenCancelMessageBatchRequest(batchId) + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"{ + ""type"": ""error"", + ""error"": { + ""type"": ""invalid_request_error"", + ""message"": ""batch: batch not found"" + } + }" + ); + + var result = await Client.CancelMessageBatchAsync(batchId); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task DeleteMessageBatchAsync_WhenCalled_ItShouldReturnDeletionResponse() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenDeleteMessageBatchRequest(batchId) + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", + ""type"": ""message_batch_deleted"" + }" + ); + + var result = await Client.DeleteMessageBatchAsync(batchId); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType(); + result.Value.Id.Should().Be(batchId); + result.Value.Type.Should().Be("message_batch_deleted"); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs index 6b6376d..223107b 100644 --- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs +++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs @@ -1,6 +1,8 @@ +using AnthropicClient.Tests.Unit; + namespace AnthropicClient.Tests.Integration; -public class IntegrationTest +public class IntegrationTest : SerializationTest { protected readonly MockHttpMessageHandler _mockHttpMessageHandler = new(); protected AnthropicApiClient Client => CreateClient(); @@ -16,6 +18,7 @@ public static class MockHttpMessageHandlerExtensions private const string BaseUrl = "https://api.anthropic.com/v1"; private static readonly string MessagesEndpoint = $"{BaseUrl}/messages"; private static readonly string CountTokensEndpoint = $"{BaseUrl}/messages/count_tokens"; + private static readonly string MessageBatchesEndpoint = $"{BaseUrl}/messages/batches"; private static readonly string ModelsEndpoint = $"{BaseUrl}/models"; private static MockedRequest SetupBaseRequest( @@ -64,4 +67,40 @@ public static class MockHttpMessageHandlerExtensions return mockHttpMessageHandler .SetupBaseRequest(HttpMethod.Get, $"{ModelsEndpoint}/{modelId}"); } + + public static MockedRequest WhenCreateMessageBatchRequest(this MockHttpMessageHandler mockHttpMessageHandler) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Post, MessageBatchesEndpoint); + } + + public static MockedRequest WhenGetMessageBatchRequest(this MockHttpMessageHandler mockHttpMessageHandler, string batchId) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Get, $"{MessageBatchesEndpoint}/{batchId}"); + } + + public static MockedRequest WhenGetMessageBatchResultsRequest(this MockHttpMessageHandler mockHttpMessageHandler, string batchId) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Get, $"{MessageBatchesEndpoint}/{batchId}/results"); + } + + public static MockedRequest WhenListMessageBatchesRequest(this MockHttpMessageHandler mockHttpMessageHandler) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Get, MessageBatchesEndpoint); + } + + public static MockedRequest WhenCancelMessageBatchRequest(this MockHttpMessageHandler mockHttpMessageHandler, string batchId) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Post, $"{MessageBatchesEndpoint}/{batchId}/cancel"); + } + + public static MockedRequest WhenDeleteMessageBatchRequest(this MockHttpMessageHandler mockHttpMessageHandler, string batchId) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Delete, $"{MessageBatchesEndpoint}/{batchId}"); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/CanceledMessageBatchResultTests.cs b/tests/AnthropicClient.Tests/Unit/Models/CanceledMessageBatchResultTests.cs new file mode 100644 index 0000000..ac68fec --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/CanceledMessageBatchResultTests.cs @@ -0,0 +1,34 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class CanceledMessageBatchResultTests : SerializationTest +{ + private const string SampleJson = @"{ + ""type"": ""canceled"" + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var result = new CanceledMessageBatchResult(); + + result.Type.Should().Be(MessageBatchResultType.Canceled); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var result = new CanceledMessageBatchResult(); + + var json = Serialize(result); + + JsonAssert.Equal(SampleJson, json); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedProperties() + { + var result = Deserialize(SampleJson); + + result!.Type.Should().Be(MessageBatchResultType.Canceled); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/ErroredMessageBatchResult.cs b/tests/AnthropicClient.Tests/Unit/Models/ErroredMessageBatchResult.cs new file mode 100644 index 0000000..0fd7f69 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/ErroredMessageBatchResult.cs @@ -0,0 +1,48 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class ErroredMessageBatchResultTests : SerializationTest +{ + private const string SampleJson = @"{ + ""type"": ""errored"", + ""error"": { + ""type"": ""error"", + ""error"": { + ""type"": ""api_error"", + ""message"": ""An error occurred."" + } + } + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var result = new ErroredMessageBatchResult(); + + result.Type.Should().Be(MessageBatchResultType.Errored); + result.Error.Error.Should().BeOfType(); + result.Error.Error.Message.Should().BeEmpty(); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var result = new ErroredMessageBatchResult + { + Error = new(new ApiError("An error occurred.")) + }; + + var json = Serialize(result); + + JsonAssert.Equal(SampleJson, json); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedProperties() + { + var result = Deserialize(SampleJson); + + result!.Type.Should().Be(MessageBatchResultType.Errored); + result.Error.Error.Should().BeOfType(); + result.Error.Error.Message.Should().Be("An error occurred."); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/ExpiredMessageBatchResultTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ExpiredMessageBatchResultTests.cs new file mode 100644 index 0000000..d8e4156 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/ExpiredMessageBatchResultTests.cs @@ -0,0 +1,34 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class ExpiredMessageBatchResultTests : SerializationTest +{ + private const string SampleJson = @"{ + ""type"": ""expired"" + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var result = new ExpiredMessageBatchResult(); + + result.Type.Should().Be(MessageBatchResultType.Expired); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var result = new ExpiredMessageBatchResult(); + + var json = Serialize(result); + + JsonAssert.Equal(SampleJson, json); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedProperties() + { + var result = Deserialize(SampleJson); + + result!.Type.Should().Be(MessageBatchResultType.Expired); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchDeleteResponseTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchDeleteResponseTests.cs new file mode 100644 index 0000000..a069ebd --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchDeleteResponseTests.cs @@ -0,0 +1,41 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageBatchDeleteResponseTests : SerializationTest +{ + private const string SampleJson = @"{ + ""id"": ""test-id"", + ""type"": ""test-type"" + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var result = new MessageBatchDeleteResponse(); + + result.Id.Should().BeEmpty(); + result.Type.Should().BeEmpty(); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var result = new MessageBatchDeleteResponse + { + Id = "test-id", + Type = "test-type" + }; + + var json = Serialize(result); + + JsonAssert.Equal(SampleJson, json); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues() + { + var result = Deserialize(SampleJson); + + result!.Id.Should().Be("test-id"); + result.Type.Should().Be("test-type"); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchRequestItemTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchRequestItemTests.cs new file mode 100644 index 0000000..2f14087 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchRequestItemTests.cs @@ -0,0 +1,36 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageBatchRequestItemTests : SerializationTest +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstanceWithPropertiesSet() + { + var customId = "custom_id"; + var messageRequest = new MessageRequest(); + + var result = new MessageBatchRequestItem(customId, messageRequest); + + result.Should().BeOfType(); + result.CustomId.Should().Be(customId); + result.Params.Should().BeSameAs(messageRequest); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Constructor_WhenCalledAndCustomIdIsInvalid_ItShouldThrowException(string? customId) + { + var act = () => new MessageBatchRequestItem(customId!, new MessageRequest()); + + act.Should().Throw(); + } + + [Fact] + public void Constructor_WhenCalledAndMessageRequestIsNull_ItShouldThrowException() + { + var act = () => new MessageBatchRequestItem("custom_id", null!); + + act.Should().Throw(); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchRequestTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchRequestTests.cs new file mode 100644 index 0000000..ccd2691 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchRequestTests.cs @@ -0,0 +1,84 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageBatchRequestTests : SerializationTest +{ + private const string SampleJson = @"{ + ""requests"": [ + { + ""custom_id"": ""my-first-request"", + ""params"": { + ""model"": ""claude-3-5-sonnet-20241022"", + ""messages"": [ + {""role"": ""user"", ""content"": [{ ""text"": ""Hello, world"", ""type"": ""text"" }]} + ], + ""max_tokens"": 1024, + ""stop_sequences"": [], + ""temperature"": 0.0, + ""stream"": false + } + }, + { + ""custom_id"": ""my-second-request"", + ""params"": { + ""model"": ""claude-3-5-sonnet-20241022"", + ""messages"": [ + {""role"": ""user"", ""content"": [{ ""text"": ""Hi again, friend"", ""type"": ""text"" }]} + ], + ""max_tokens"": 1024, + ""stop_sequences"": [], + ""temperature"": 0.0, + ""stream"": false + } + } + ] + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstanceWithPropertiesSet() + { + var requests = new List { new("custom_id", new()) }; + + var result = new MessageBatchRequest(requests); + + result.Should().BeOfType(); + result.Requests.Should().BeSameAs(requests); + } + + [Fact] + public void Constructor_WhenCalledWithEmptyRequests_ItShouldThrowException() + { + var act = () => new MessageBatchRequest([]); + + act.Should().Throw(); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var requests = new List + { + new("my-first-request", new() + { + Model = "claude-3-5-sonnet-20241022", + MaxTokens = 1024, + Messages = [ + new() { Role = "user", Content = [new TextContent("Hello, world")] } + ] + }), + new("my-second-request", new() + { + Model = "claude-3-5-sonnet-20241022", + MaxTokens = 1024, + Messages = [ + new() { Role = "user", Content = [new TextContent("Hi again, friend")] } + ] + }) + }; + + var result = new MessageBatchRequest(requests); + + var json = Serialize(result); + + JsonAssert.Equal(SampleJson, json); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResponseTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResponseTests.cs new file mode 100644 index 0000000..5001b4d --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResponseTests.cs @@ -0,0 +1,115 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageBatchResponseTests : SerializationTest +{ + private const string SampleJson = @"{ + ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", + ""type"": ""message_batch"", + ""processing_status"": ""in_progress"", + ""request_counts"": { + ""processing"": 100, + ""succeeded"": 50, + ""errored"": 30, + ""canceled"": 10, + ""expired"": 10 + }, + ""ended_at"": ""2024-08-20T18:37:24.100435Z"", + ""created_at"": ""2024-08-20T18:37:24.100435Z"", + ""expires_at"": ""2024-08-20T18:37:24.100435Z"", + ""archived_at"": ""2024-08-20T18:37:24.100435Z"", + ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", + ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" + }"; + + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstanceWithPropertiesSet() + { + var result = new MessageBatchResponse(); + + result.Should().BeOfType(); + result.Id.Should().BeEmpty(); + result.Type.Should().BeEmpty(); + result.ProcessingStatus.Should().BeEmpty(); + result.RequestCounts.Should().BeEquivalentTo(new MessageBatchRequestCounts()); + result.EndedAt.Should().Be(DateTimeOffset.MinValue); + result.CreatedAt.Should().Be(DateTimeOffset.MinValue); + result.ExpiresAt.Should().Be(DateTimeOffset.MinValue); + result.ArchivedAt.Should().Be(DateTimeOffset.MinValue); + result.CancelInitiatedAt.Should().Be(DateTimeOffset.MinValue); + result.ResultsUrl.Should().BeEmpty(); + } + + [Fact] + public void JsonSerialization_WhenDeserialized_ItShouldHaveExpectedValues() + { + var result = Deserialize(SampleJson); + + result.Should().BeEquivalentTo(new MessageBatchResponse + { + Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", + Type = "message_batch", + ProcessingStatus = "in_progress", + RequestCounts = new() + { + Processing = 100, + Succeeded = 50, + Errored = 30, + Canceled = 10, + Expired = 10 + }, + EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" + }); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var expectedJson = @"{ + ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", + ""type"": ""message_batch"", + ""processing_status"": ""in_progress"", + ""request_counts"": { + ""processing"": 100, + ""succeeded"": 50, + ""errored"": 30, + ""canceled"": 10, + ""expired"": 10 + }, + ""ended_at"": ""2024-08-20T18:37:24.100435+00:00"", + ""created_at"": ""2024-08-20T18:37:24.100435+00:00"", + ""expires_at"": ""2024-08-20T18:37:24.100435+00:00"", + ""archived_at"": ""2024-08-20T18:37:24.100435+00:00"", + ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435+00:00"", + ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" + }"; + + var result = Serialize(new MessageBatchResponse + { + Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", + Type = "message_batch", + ProcessingStatus = "in_progress", + RequestCounts = new() + { + Processing = 100, + Succeeded = 50, + Errored = 30, + Canceled = 10, + Expired = 10 + }, + EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), + ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" + }); + + JsonAssert.Equal(expectedJson, result); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultItemTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultItemTests.cs new file mode 100644 index 0000000..5023018 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultItemTests.cs @@ -0,0 +1,14 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageBatchResultItemTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldReturnInstanceWithPropertiesSet() + { + var result = new MessageBatchResultItem(); + + result.Should().BeOfType(); + result.CustomId.Should().BeEmpty(); + result.Result.Should().Be(default); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTests.cs new file mode 100644 index 0000000..dd873d6 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTests.cs @@ -0,0 +1,25 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageBatchResultTests : SerializationTest +{ + [Fact] + public void JsonDeserialization_WhenHasUnknownType_ItShouldThrowException() + { + var json = @"{""type"":""unknown""}"; + + var action = () => Deserialize(json); + + action.Should().Throw(); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var expectedJson = @"{""type"":""expired""}"; + var messageBatchResult = new ExpiredMessageBatchResult(); + + var json = Serialize(messageBatchResult); + + JsonAssert.Equal(expectedJson, json); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTypeTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTypeTests.cs new file mode 100644 index 0000000..88381c5 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTypeTests.cs @@ -0,0 +1,36 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageBatchResultTypeTests +{ + [Fact] + public void Succeeded_WhenCalled_ItShouldReturnExpectedValue() + { + var result = MessageBatchResultType.Succeeded; + + result.Should().Be("succeeded"); + } + + [Fact] + public void Errored_WhenCalled_ItShouldReturnExpectedValue() + { + var result = MessageBatchResultType.Errored; + + result.Should().Be("errored"); + } + + [Fact] + public void Canceled_WhenCalled_ItShouldReturnExpectedValue() + { + var result = MessageBatchResultType.Canceled; + + result.Should().Be("canceled"); + } + + [Fact] + public void Expired_WhenCalled_ItShouldReturnExpectedValue() + { + var result = MessageBatchResultType.Expired; + + result.Should().Be("expired"); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchStatusTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchStatusTests.cs new file mode 100644 index 0000000..316a403 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchStatusTests.cs @@ -0,0 +1,22 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class MessageBatchStatusTests +{ + [Fact] + public void Canceling_WhenCalled_ItShouldReturnCancelingStatus() + { + MessageBatchStatus.Canceling.Should().Be("canceling"); + } + + [Fact] + public void InProgress_WhenCalled_ItShouldReturnCancelingStatus() + { + MessageBatchStatus.InProgress.Should().Be("in_progress"); + } + + [Fact] + public void Ended_WhenCalled_ItShouldReturnCancelingStatus() + { + MessageBatchStatus.Ended.Should().Be("ended"); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/SucceededMessageBatchResultTests.cs b/tests/AnthropicClient.Tests/Unit/Models/SucceededMessageBatchResultTests.cs new file mode 100644 index 0000000..95ce4a0 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/SucceededMessageBatchResultTests.cs @@ -0,0 +1,95 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class SucceededMessageBatchResultTests : SerializationTest +{ + private const string SampleJson = @"{ + ""message"": { + ""id"": ""msg_01FqfsLoHwgeFbguDgpz48m7"", + ""model"": ""claude-3-5-sonnet-20240620"", + ""role"": ""assistant"", + ""stop_reason"": ""end_turn"", + ""type"": ""message"", + ""usage"": { + ""input_tokens"": 10, + ""output_tokens"": 34, + ""cache_creation_input_tokens"": 0, + ""cache_read_input_tokens"": 0 + }, + ""content"": [ + { + ""text"": ""Hello! How can I assist you today? Feel free to ask me any questions or let me know if there\u0027s anything you\u0027d like to chat about."", + ""type"": ""text"" + } + ] + }, + ""type"": ""succeeded"" + }"; + + [Fact] + public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet() + { + var result = new SucceededMessageBatchResult(); + + result.Type.Should().Be(MessageBatchResultType.Succeeded); + result.Message.Should().BeEquivalentTo(new MessageResponse()); + } + + [Fact] + public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() + { + var result = new SucceededMessageBatchResult + { + Message = new MessageResponse + { + Id = "msg_01FqfsLoHwgeFbguDgpz48m7", + Type = "message", + Role = "assistant", + Model = "claude-3-5-sonnet-20240620", + Content = [ + new TextContent() + { + Text = "Hello! How can I assist you today? Feel free to ask me any questions or let me know if there's anything you'd like to chat about." + } + ], + StopReason = "end_turn", + Usage = new() + { + InputTokens = 10, + OutputTokens = 34 + } + } + }; + + var json = Serialize(result); + + JsonAssert.Equal(SampleJson, json); + } + + [Fact] + public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues() + { + var result = Deserialize(SampleJson); + + result!.Type.Should().Be(MessageBatchResultType.Succeeded); + result.Message.Should().BeEquivalentTo(new MessageResponse + { + Id = "msg_01FqfsLoHwgeFbguDgpz48m7", + Type = "message", + Role = "assistant", + Model = "claude-3-5-sonnet-20240620", + Content = [ + new TextContent() + { + Text = "Hello! How can I assist you today? Feel free to ask me any questions or let me know if there's anything you'd like to chat about." + } + ], + StopReason = "end_turn", + StopSequence = null, + Usage = new() + { + InputTokens = 10, + OutputTokens = 34 + } + }); + } +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/ToolCallTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ToolCallTests.cs index 97240a4..a338a05 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/ToolCallTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/ToolCallTests.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Nodes; - namespace AnthropicClient.Tests.Unit.Models; public class ToolCallTests : SerializationTest diff --git a/tests/AnthropicClient.Tests/Unit/SerializationTest.cs b/tests/AnthropicClient.Tests/Unit/SerializationTest.cs index 06dd487..8fdd0df 100644 --- a/tests/AnthropicClient.Tests/Unit/SerializationTest.cs +++ b/tests/AnthropicClient.Tests/Unit/SerializationTest.cs @@ -1,5 +1,3 @@ -using AnthropicClient.Json; - namespace AnthropicClient.Tests.Unit; public class SerializationTest