Merge pull request #31 from StevanFreeborn/stevanfreeborn/feat/add-support-for-message-batching

feat: add support for message batching api
This commit is contained in:
Stevan Freeborn
2025-01-15 10:31:54 -06:00
committed by GitHub
38 changed files with 2499 additions and 114 deletions
+190
View File
@@ -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);
```
+118 -93
View File
@@ -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;
/// <summary>
/// Represents a client for interacting with the Anthropic API.
/// </summary>
public interface IAnthropicApiClient
{
/// <summary>
/// Creates a message asynchronously.
/// </summary>
/// <param name="request">The message request to create.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/>.</returns>
Task<AnthropicResult<MessageResponse>> CreateMessageAsync(MessageRequest request);
/// <summary>
/// Creates a message asynchronously and streams the response.
/// </summary>
/// <param name="request">The message request to create.</param>
/// <returns>An asynchronous enumerable that yields the response event by event.</returns>
IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRequest request);
/// <summary>
/// Counts the tokens in a message asynchronously.
/// </summary>
/// <param name="request">The count message tokens request.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="TokenCountResponse"/>.</returns>
Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request);
/// <summary>
/// Lists the models asynchronously.
/// </summary>
/// <param name="request">The paging request to use for listing the models.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="AnthropicModel"/>.</returns>
Task<AnthropicResult<Page<AnthropicModel>>> ListModelsAsync(PagingRequest? request = null);
/// <summary>
/// Lists the models asynchronously
/// </summary>
/// <param name="limit">The maximum number of models to return in each page.</param>
/// <returns>An asynchronous enumerable that yields the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="AnthropicModel"/>.</returns>
///
IAsyncEnumerable<AnthropicResult<Page<AnthropicModel>>> ListAllModelsAsync(int limit = 20);
/// <summary>
/// Gets a model by its ID asynchronously.
/// </summary>
/// <param name="modelId">The ID of the model to get.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicModel"/>.</returns>
Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId);
}
/// <inheritdoc cref="IAnthropicApiClient"/>
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
}
/// <inheritdoc/>
public async Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request)
public async Task<AnthropicResult<MessageBatchResponse>> CreateMessageBatchAsync(MessageBatchRequest request)
{
var response = await SendRequestAsync(CountTokensEndpoint, request);
var response = await SendRequestAsync(MessageBatchesEndpoint, request);
return await CreateResultAsync<MessageBatchResponse>(response);
}
/// <inheritdoc/>
public async Task<AnthropicResult<MessageBatchResponse>> GetMessageBatchAsync(string batchId)
{
var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}");
return await CreateResultAsync<MessageBatchResponse>(response);
}
/// <inheritdoc/>
public async Task<AnthropicResult<Page<MessageBatchResponse>>> ListMessageBatchesAsync(PagingRequest? request = null)
{
var pagingRequest = request ?? new PagingRequest();
var endpoint = $"{MessageBatchesEndpoint}?{pagingRequest.ToQueryParameters()}";
var response = await SendRequestAsync(endpoint);
return await CreateResultAsync<Page<MessageBatchResponse>>(response);
}
/// <inheritdoc/>
public async IAsyncEnumerable<AnthropicResult<Page<MessageBatchResponse>>> ListAllMessageBatchesAsync(int limit = 20)
{
await foreach (var result in GetAllPagesAsync<MessageBatchResponse>(MessageBatchesEndpoint, limit))
{
yield return result;
}
}
/// <inheritdoc/>
public async Task<AnthropicResult<MessageBatchResponse>> CancelMessageBatchAsync(string batchId)
{
var endpoint = $"{MessageBatchesEndpoint}/{batchId}/cancel";
var response = await SendRequestAsync(endpoint, HttpMethod.Post);
return await CreateResultAsync<MessageBatchResponse>(response);
}
/// <inheritdoc/>
public async Task<AnthropicResult<MessageBatchDeleteResponse>> DeleteMessageBatchAsync(string batchId)
{
var endpoint = $"{MessageBatchesEndpoint}/{batchId}";
var response = await SendRequestAsync(endpoint, HttpMethod.Delete);
return await CreateResultAsync<MessageBatchDeleteResponse>(response);
}
/// <inheritdoc/>
public async Task<AnthropicResult<IAsyncEnumerable<MessageBatchResultItem>>> 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<AnthropicError>(responseContent) ?? new AnthropicError();
return AnthropicResult<TokenCountResponse>.Failure(error, anthropicHeaders);
var content = await response.Content.ReadAsStringAsync();
var error = Deserialize<AnthropicError>(content) ?? new AnthropicError();
return AnthropicResult<IAsyncEnumerable<MessageBatchResultItem>>.Failure(error, anthropicHeaders);
}
var msgResponse = Deserialize<TokenCountResponse>(responseContent) ?? new TokenCountResponse();
return AnthropicResult<TokenCountResponse>.Success(msgResponse, anthropicHeaders);
return AnthropicResult<IAsyncEnumerable<MessageBatchResultItem>>.Success(ReadResultsAsync(), anthropicHeaders);
async IAsyncEnumerable<MessageBatchResultItem> 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<MessageBatchResultItem>(line) ?? new MessageBatchResultItem();
yield return resultItem;
line = await streamReader.ReadLineAsync();
}
}
}
/// <inheritdoc/>
public async Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request)
{
var response = await SendRequestAsync(CountTokensEndpoint, request);
return await CreateResultAsync<TokenCountResponse>(response);
}
/// <inheritdoc/>
@@ -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<AnthropicError>(responseContent) ?? new AnthropicError();
return AnthropicResult<Page<AnthropicModel>>.Failure(error, anthropicHeaders);
}
var page = Deserialize<Page<AnthropicModel>>(responseContent) ?? new Page<AnthropicModel>();
return AnthropicResult<Page<AnthropicModel>>.Success(page, anthropicHeaders);
return await CreateResultAsync<Page<AnthropicModel>>(response);
}
/// <inheritdoc/>
public async IAsyncEnumerable<AnthropicResult<Page<AnthropicModel>>> ListAllModelsAsync(int limit = 20)
{
await foreach (var result in GetAllPagesAsync<AnthropicModel>(ModelsEndpoint, limit))
{
yield return result;
}
}
/// <inheritdoc/>
public async Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId)
{
var endpoint = $"{ModelsEndpoint}/{modelId}";
var response = await SendRequestAsync(endpoint);
return await CreateResultAsync<AnthropicModel>(response);
}
private async IAsyncEnumerable<AnthropicResult<Page<T>>> GetAllPagesAsync<T>(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<AnthropicError>(responseContent) ?? new AnthropicError();
yield return AnthropicResult<Page<AnthropicModel>>.Failure(error, anthropicHeaders);
yield return AnthropicResult<Page<T>>.Failure(error, anthropicHeaders);
yield break;
}
var page = Deserialize<Page<AnthropicModel>>(responseContent) ?? new Page<AnthropicModel>();
var page = Deserialize<Page<T>>(responseContent) ?? new Page<T>();
if (page.HasMore && page.LastId is not null)
{
@@ -355,28 +382,10 @@ public class AnthropicApiClient : IAnthropicApiClient
hasMore = false;
}
yield return AnthropicResult<Page<AnthropicModel>>.Success(page, anthropicHeaders);
yield return AnthropicResult<Page<T>>.Success(page, anthropicHeaders);
} while (hasMore);
}
/// <inheritdoc/>
public async Task<AnthropicResult<AnthropicModel>> 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<AnthropicError>(responseContent) ?? new AnthropicError();
return AnthropicResult<AnthropicModel>.Failure(error, anthropicHeaders);
}
var model = Deserialize<AnthropicModel>(responseContent) ?? new AnthropicModel();
return AnthropicResult<AnthropicModel>.Success(model, anthropicHeaders);
}
private ToolCall? GetToolCall(MessageResponse response, List<Tool> tools)
{
var toolUse = response.Content.OfType<ToolUseContent>().FirstOrDefault();
@@ -396,9 +405,25 @@ public class AnthropicApiClient : IAnthropicApiClient
return new ToolCall(tool, toolUse);
}
private async Task<HttpResponseMessage> SendRequestAsync(string endpoint)
private async Task<AnthropicResult<T>> CreateResultAsync<T>(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<AnthropicError>(responseContent) ?? new AnthropicError();
return AnthropicResult<T>.Failure(error, anthropicHeaders);
}
var model = Deserialize<T>(responseContent) ?? new T();
return AnthropicResult<T>.Success(model, anthropicHeaders);
}
private async Task<HttpResponseMessage> SendRequestAsync(string endpoint, HttpMethod? method = null)
{
var request = new HttpRequestMessage(method ?? HttpMethod.Get, endpoint);
return await _httpClient.SendAsync(request);
}
private async Task<HttpResponseMessage> SendRequestAsync<T>(string endpoint, T request)
+101
View File
@@ -0,0 +1,101 @@
using AnthropicClient.Models;
namespace AnthropicClient;
/// <summary>
/// Represents a client for interacting with the Anthropic API.
/// </summary>
public interface IAnthropicApiClient
{
/// <summary>
/// Creates a message asynchronously.
/// </summary>
/// <param name="request">The message request to create.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/>.</returns>
Task<AnthropicResult<MessageResponse>> CreateMessageAsync(MessageRequest request);
/// <summary>
/// Creates a message asynchronously and streams the response.
/// </summary>
/// <param name="request">The message request to create.</param>
/// <returns>An asynchronous enumerable that yields the response event by event.</returns>
IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRequest request);
/// <summary>
/// Creates a batch of messages asynchronously.
/// </summary>
/// <param name="request">The message batch request to create.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="MessageBatchResponse"/>.</returns>
Task<AnthropicResult<MessageBatchResponse>> CreateMessageBatchAsync(MessageBatchRequest request);
/// <summary>
/// Gets a message batch asynchronously.
/// </summary>
/// <param name="batchId">The ID of the message batch to get.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="MessageBatchResponse"/>.</returns>
Task<AnthropicResult<MessageBatchResponse>> GetMessageBatchAsync(string batchId);
/// <summary>
/// Lists the message batches asynchronously.
/// </summary>
/// <param name="request">The paging request to use for listing the message batches.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="MessageBatchResponse"/>.</returns>
Task<AnthropicResult<Page<MessageBatchResponse>>> ListMessageBatchesAsync(PagingRequest? request = null);
/// <summary>
/// Lists all message batches asynchronously.
/// </summary>
/// <param name="limit">The maximum number of message batches to return in each page.</param>
/// <returns>An asynchronous enumerable that yields the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="MessageBatchResponse"/>.</returns>
IAsyncEnumerable<AnthropicResult<Page<MessageBatchResponse>>> ListAllMessageBatchesAsync(int limit = 20);
/// <summary>
/// Cancels a message batch asynchronously.
/// </summary>
/// <param name="batchId">The ID of the message batch to cancel.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="MessageBatchResponse"/>.</returns>
Task<AnthropicResult<MessageBatchResponse>> CancelMessageBatchAsync(string batchId);
/// <summary>
/// Deletes a message batch asynchronously.
/// </summary>
/// <param name="batchId">The ID of the message batch to delete.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="MessageBatchDeleteResponse"/>.</returns>
Task<AnthropicResult<MessageBatchDeleteResponse>> DeleteMessageBatchAsync(string batchId);
/// <summary>
/// Gets the results of a message batch asynchronously.
/// </summary>
/// <param name="batchId">The ID of the message batch to get the results for.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="IAsyncEnumerable{T}"/> where T is <see cref="MessageBatchResultItem"/>.</returns>
Task<AnthropicResult<IAsyncEnumerable<MessageBatchResultItem>>> GetMessageBatchResultsAsync(string batchId);
/// <summary>
/// Counts the tokens in a message asynchronously.
/// </summary>
/// <param name="request">The count message tokens request.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="TokenCountResponse"/>.</returns>
Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request);
/// <summary>
/// Lists the models asynchronously.
/// </summary>
/// <param name="request">The paging request to use for listing the models.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="AnthropicModel"/>.</returns>
Task<AnthropicResult<Page<AnthropicModel>>> ListModelsAsync(PagingRequest? request = null);
/// <summary>
/// Lists the models asynchronously
/// </summary>
/// <param name="limit">The maximum number of models to return in each page.</param>
/// <returns>An asynchronous enumerable that yields the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="AnthropicModel"/>.</returns>
///
IAsyncEnumerable<AnthropicResult<Page<AnthropicModel>>> ListAllModelsAsync(int limit = 20);
/// <summary>
/// Gets a model by its ID asynchronously.
/// </summary>
/// <param name="modelId">The ID of the model to get.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicModel"/>.</returns>
Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId);
}
@@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -17,6 +17,7 @@ static class JsonSerializationOptions
new EventDataConverter(),
new ContentDeltaConverter(),
new JsonStringEnumConverter(),
new MessageBatchResultConverter(),
},
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
@@ -0,0 +1,29 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AnthropicClient.Models;
namespace AnthropicClient.Json;
class MessageBatchResultConverter : JsonConverter<MessageBatchResult>
{
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<SucceededMessageBatchResult>(root.GetRawText(), options)!,
MessageBatchResultType.Errored => JsonSerializer.Deserialize<ErroredMessageBatchResult>(root.GetRawText(), options)!,
MessageBatchResultType.Canceled => JsonSerializer.Deserialize<CanceledMessageBatchResult>(root.GetRawText(), options)!,
MessageBatchResultType.Expired => JsonSerializer.Deserialize<ExpiredMessageBatchResult>(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);
}
}
@@ -0,0 +1,14 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents a message batch result that was cancelled.
/// </summary>
public class CanceledMessageBatchResult : MessageBatchResult
{
/// <summary>
/// Initializes a new instance of the <see cref="CanceledMessageBatchResult"/> class.
/// </summary>
public CanceledMessageBatchResult() : base(MessageBatchResultType.Canceled)
{
}
}
@@ -0,0 +1,19 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents a message batch result that contains an error response.
/// </summary>
public class ErroredMessageBatchResult : MessageBatchResult
{
/// <summary>
/// Gets the error of the message batch result.
/// </summary>
public AnthropicError Error { get; init; } = new AnthropicError();
/// <summary>
/// Initializes a new instance of the <see cref="ErroredMessageBatchResult"/> class.
/// </summary>
public ErroredMessageBatchResult() : base(MessageBatchResultType.Errored)
{
}
}
@@ -0,0 +1,14 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents a message batch result that has expired.
/// </summary>
public class ExpiredMessageBatchResult : MessageBatchResult
{
/// <summary>
/// Initializes a new instance of the <see cref="ExpiredMessageBatchResult"/> class.
/// </summary>
public ExpiredMessageBatchResult() : base(MessageBatchResultType.Expired)
{
}
}
@@ -0,0 +1,17 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents a message batch delete response.
/// </summary>
public class MessageBatchDeleteResponse
{
/// <summary>
/// Gets the ID of the message batch that was deleted.
/// </summary>
public string Id { get; init; } = string.Empty;
/// <summary>
/// Gets the type of the message batch response.
/// </summary>
public string Type { get; init; } = string.Empty;
}
@@ -0,0 +1,28 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents a request to create a batch of messages.
/// </summary>
public class MessageBatchRequest
{
/// <summary>
/// Gets the requests to create messages.
/// </summary>
public List<MessageBatchRequestItem> Requests { get; init; } = [];
/// <summary>
/// Initializes a new instance of the <see cref="MessageBatchRequest"/> class.
/// </summary>
/// <param name="requests">The requests to create messages.</param>
/// <exception cref="ArgumentException">Thrown when <paramref name="requests"/> is empty.</exception>
/// <returns>An instance of the <see cref="MessageBatchRequest"/> class.</returns>
public MessageBatchRequest(List<MessageBatchRequestItem> requests)
{
if (requests.Count == 0)
{
throw new ArgumentException($"{nameof(requests)} must not be empty.", nameof(requests));
}
Requests = requests;
}
}
@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents an item in a batch of messages.
/// </summary>
public class MessageBatchRequestItem
{
/// <summary>
/// Gets the custom identifier for the message.
/// </summary>
[JsonPropertyName("custom_id")]
public string CustomId { get; init; }
/// <summary>
/// Gets the message request parameters.
/// </summary>
public MessageRequest Params { get; init; }
/// <summary>
/// Initializes a new instance of the <see cref="MessageBatchRequestItem"/> class.
/// </summary>
/// <param name="customId">The custom identifier for the message.</param>
/// <param name="messageRequest">The message request parameters.</param>
/// <exception cref="ArgumentException">Thrown when <paramref name="customId"/> is null or whitespace.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="messageRequest"/> is null.</exception>
/// <returns>An instance of the <see cref="MessageBatchRequestItem"/> class.</returns>
public MessageBatchRequestItem(string customId, MessageRequest messageRequest)
{
ArgumentValidator.ThrowIfNullOrWhitespace(customId, nameof(customId));
ArgumentValidator.ThrowIfNull(messageRequest, nameof(messageRequest));
CustomId = customId;
Params = messageRequest;
}
}
@@ -0,0 +1,98 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a response to a batch of messages.
/// </summary>
public class MessageBatchResponse
{
/// <summary>
/// Gets the identifier of the batch.
/// </summary>
public string Id { get; init; } = string.Empty;
/// <summary>
/// Gets the type of the batch.
/// </summary>
public string Type { get; init; } = string.Empty;
/// <summary>
/// Gets the processing status of the batch.
/// </summary>
[JsonPropertyName("processing_status")]
public string ProcessingStatus { get; init; } = string.Empty;
/// <summary>
/// Gets the counts of requests in the batch.
/// </summary>
[JsonPropertyName("request_counts")]
public MessageBatchRequestCounts RequestCounts { get; init; } = new();
/// <summary>
/// Gets the date and time when the batch ended.
/// </summary>
[JsonPropertyName("ended_at")]
public DateTimeOffset? EndedAt { get; init; } = DateTimeOffset.MinValue;
/// <summary>
/// Gets the date and time when the batch was created.
/// </summary>
[JsonPropertyName("created_at")]
public DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Gets the date and time when the batch expires.
/// </summary>
[JsonPropertyName("expires_at")]
public DateTimeOffset ExpiresAt { get; init; }
/// <summary>
/// Gets the date and time when the batch was archived.
/// </summary>
[JsonPropertyName("archived_at")]
public DateTimeOffset? ArchivedAt { get; init; } = DateTimeOffset.MinValue;
/// <summary>
/// Gets the date and time when the batch cancellation was initiated.
/// </summary>
[JsonPropertyName("cancel_initiated_at")]
public DateTimeOffset? CancelInitiatedAt { get; init; } = DateTimeOffset.MinValue;
/// <summary>
/// Gets the URL to the results of the batch.
/// </summary>
[JsonPropertyName("results_url")]
public string? ResultsUrl { get; init; } = string.Empty;
}
/// <summary>
/// Represents the counts of requests in a batch of messages.
/// </summary>
public class MessageBatchRequestCounts
{
/// <summary>
/// Gets the number of requests in the batch that are processing.
/// </summary>
public int Processing { get; init; }
/// <summary>
/// Gets the number of requests in the batch that succeeded.
/// </summary>
public int Succeeded { get; init; }
/// <summary>
/// Gets the number of requests in the batch that errored.
/// </summary>
public int Errored { get; init; }
/// <summary>
/// Gets the number of requests in the batch that were cancelled.
/// </summary>
public int Canceled { get; init; }
/// <summary>
/// Gets the number of requests in the batch that expired.
/// </summary>
public int Expired { get; init; }
}
@@ -0,0 +1,26 @@
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a message batch result.
/// </summary>
public abstract class MessageBatchResult
{
/// <summary>
/// Gets the type of the message batch result.
/// </summary>
public string Type { get; init; } = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="MessageBatchResult"/> class.
/// </summary>
/// <param name="type">The type of the message batch result.</param>
/// <returns>An instance of the <see cref="MessageBatchResult"/> class.</returns>
public MessageBatchResult(string type)
{
ArgumentValidator.ThrowIfNullOrWhitespace(type, nameof(type));
Type = type;
}
}
@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a message batch result item.
/// </summary>
public class MessageBatchResultItem
{
/// <summary>
/// Gets the custom ID of the message batch result item.
/// </summary>
[JsonPropertyName("custom_id")]
public string CustomId { get; init; } = string.Empty;
/// <summary>
/// Gets the result of the message batch result item.
/// </summary>
public MessageBatchResult Result { get; init; } = default!;
}
@@ -0,0 +1,27 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the types of message batch results.
/// </summary>
public static class MessageBatchResultType
{
/// <summary>
/// Represents a succeeded message batch result.
/// </summary>
public const string Succeeded = "succeeded";
/// <summary>
/// Represents an errored message batch result.
/// </summary>
public const string Errored = "errored";
/// <summary>
/// Represents a canceled message batch result.
/// </summary>
public const string Canceled = "canceled";
/// <summary>
/// Represents an expired message batch result.
/// </summary>
public const string Expired = "expired";
}
@@ -0,0 +1,22 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the status of a message batch.
/// </summary>
public static class MessageBatchStatus
{
/// <summary>
/// The status of a message batch that is being canceled.
/// </summary>
public const string Canceling = "canceling";
/// <summary>
/// The status of a message batch that is in progress.
/// </summary>
public const string InProgress = "in_progress";
/// <summary>
/// The status of a message batch that has ended.
/// </summary>
public const string Ended = "ended";
}
@@ -0,0 +1,20 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents a message batch result that contains a message response.
/// </summary>
public class SucceededMessageBatchResult : MessageBatchResult
{
/// <summary>
/// Gets the message of the message batch result.
/// </summary>
public MessageResponse Message { get; init; } = new MessageResponse();
/// <summary>
/// Initializes a new instance of the <see cref="SucceededMessageBatchResult"/> class.
/// </summary>
/// <returns>An instance of the <see cref="SucceededMessageBatchResult"/> class.</returns>
public SucceededMessageBatchResult() : base(MessageBatchResultType.Succeeded)
{
}
}
@@ -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<AnthropicModel>();
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<MessageBatchResponse>();
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<MessageBatchResponse>();
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<Page<MessageBatchResponse>>();
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<MessageBatchResponse>();
result.Value.Id.Should().Be(createResult.Value.Id);
result.Value.ProcessingStatus.Should().Be(MessageBatchStatus.Canceling);
}
}
@@ -0,0 +1,7 @@
namespace AnthropicClient.Tests.Files;
static class TestFileHelper
{
public static string GetTestFilePath(string fileName) =>
Path.Combine(Directory.GetCurrentDirectory(), "Files", fileName);
}
@@ -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}}}}
File diff suppressed because it is too large Load Diff
@@ -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}");
}
}
@@ -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<CanceledMessageBatchResult>(SampleJson);
result!.Type.Should().Be(MessageBatchResultType.Canceled);
}
}
@@ -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<ApiError>();
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<ErroredMessageBatchResult>(SampleJson);
result!.Type.Should().Be(MessageBatchResultType.Errored);
result.Error.Error.Should().BeOfType<ApiError>();
result.Error.Error.Message.Should().Be("An error occurred.");
}
}
@@ -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<ExpiredMessageBatchResult>(SampleJson);
result!.Type.Should().Be(MessageBatchResultType.Expired);
}
}
@@ -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<MessageBatchDeleteResponse>(SampleJson);
result!.Id.Should().Be("test-id");
result.Type.Should().Be("test-type");
}
}
@@ -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<MessageBatchRequestItem>();
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<ArgumentException>();
}
[Fact]
public void Constructor_WhenCalledAndMessageRequestIsNull_ItShouldThrowException()
{
var act = () => new MessageBatchRequestItem("custom_id", null!);
act.Should().Throw<ArgumentException>();
}
}
@@ -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<MessageBatchRequestItem> { new("custom_id", new()) };
var result = new MessageBatchRequest(requests);
result.Should().BeOfType<MessageBatchRequest>();
result.Requests.Should().BeSameAs(requests);
}
[Fact]
public void Constructor_WhenCalledWithEmptyRequests_ItShouldThrowException()
{
var act = () => new MessageBatchRequest([]);
act.Should().Throw<ArgumentException>();
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var requests = new List<MessageBatchRequestItem>
{
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);
}
}
@@ -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<MessageBatchResponse>();
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<MessageBatchResponse>(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);
}
}
@@ -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<MessageBatchResultItem>();
result.CustomId.Should().BeEmpty();
result.Result.Should().Be(default);
}
}
@@ -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<MessageBatchResult>(json);
action.Should().Throw<JsonException>();
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var expectedJson = @"{""type"":""expired""}";
var messageBatchResult = new ExpiredMessageBatchResult();
var json = Serialize<MessageBatchResult>(messageBatchResult);
JsonAssert.Equal(expectedJson, json);
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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<SucceededMessageBatchResult>(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
}
});
}
}
@@ -1,5 +1,3 @@
using System.Text.Json.Nodes;
namespace AnthropicClient.Tests.Unit.Models;
public class ToolCallTests : SerializationTest
@@ -1,5 +1,3 @@
using AnthropicClient.Json;
namespace AnthropicClient.Tests.Unit;
public class SerializationTest