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