From 9548754d6cab73d21c7b7a28264374e79dcb8bab Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 8 Jan 2025 20:37:58 -0600
Subject: [PATCH 01/22] feat: implement CreateMessageBatchAsync method
---
src/AnthropicClient/AnthropicApiClient.cs | 27 +++-
.../Models/MessageBatchRequest.cs | 28 +++++
.../Models/MessageBatchRequestItem.cs | 39 ++++++
.../Models/MessageBatchResponse.cs | 98 +++++++++++++++
.../Integration/AnthropicApiClientTests.cs | 54 ++++++++
.../Integration/IntegrationTest.cs | 7 ++
.../Models/MessageBatchRequestItemTests.cs | 36 ++++++
.../Unit/Models/MessageBatchRequestTests.cs | 84 +++++++++++++
.../Unit/Models/MessageBatchResponseTests.cs | 115 ++++++++++++++++++
9 files changed, 487 insertions(+), 1 deletion(-)
create mode 100644 src/AnthropicClient/Models/MessageBatchRequest.cs
create mode 100644 src/AnthropicClient/Models/MessageBatchRequestItem.cs
create mode 100644 src/AnthropicClient/Models/MessageBatchResponse.cs
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/MessageBatchRequestItemTests.cs
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/MessageBatchRequestTests.cs
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/MessageBatchResponseTests.cs
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index 3be31ee..831161d 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -28,6 +28,13 @@ public interface IAnthropicApiClient
/// 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);
+
///
/// Counts the tokens in a message asynchronously.
///
@@ -64,7 +71,8 @@ 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:";
@@ -287,6 +295,23 @@ public class AnthropicApiClient : IAnthropicApiClient
} while (true);
}
+ ///
+ public async Task> CreateMessageBatchAsync(MessageBatchRequest request)
+ {
+ var response = await SendRequestAsync(MessageBatchesEndpoint, request);
+ var anthropicHeaders = new AnthropicHeaders(response.Headers);
+ var responseContent = await response.Content.ReadAsStringAsync();
+
+ if (response.IsSuccessStatusCode is false)
+ {
+ var error = Deserialize(responseContent) ?? new AnthropicError();
+ return AnthropicResult.Failure(error, anthropicHeaders);
+ }
+
+ var msgBatchResponse = Deserialize(responseContent) ?? new MessageBatchResponse();
+ return AnthropicResult.Success(msgBatchResponse, anthropicHeaders);
+ }
+
///
public async Task> CountMessageTokensAsync(CountMessageTokensRequest request)
{
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..b3c0a0e
--- /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 MessageBatchRequestCounts();
+
+ ///
+ /// Gets the date and time when the batch ended.
+ ///
+ [JsonPropertyName("ended_at")]
+ public DateTimeOffset EndedAt { get; init; }
+
+ ///
+ /// 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; }
+
+ ///
+ /// Gets the date and time when the batch cancellation was initiated.
+ ///
+ [JsonPropertyName("cancel_initiated_at")]
+ public DateTimeOffset CancelInitiatedAt { get; init; }
+
+ ///
+ /// 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/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
index eaef353..ca97570 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -978,4 +978,58 @@ 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");
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
index 6b6376d..1a9ef1c 100644
--- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
+++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
@@ -16,6 +16,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 +65,10 @@ public static class MockHttpMessageHandlerExtensions
return mockHttpMessageHandler
.SetupBaseRequest(HttpMethod.Get, $"{ModelsEndpoint}/{modelId}");
}
+
+ public static MockedRequest WhenCreateMessageBatchRequest(this MockHttpMessageHandler mockHttpMessageHandler)
+ {
+ return mockHttpMessageHandler
+ .SetupBaseRequest(HttpMethod.Post, MessageBatchesEndpoint);
+ }
}
\ 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..4cde6a1
--- /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 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 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 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"
+ });
+
+ JsonAssert.Equal(expectedJson, result);
+ }
+}
\ No newline at end of file
From 5e247a3c3afb9a3dad954eaa5890de106ae2edb9 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 8 Jan 2025 20:39:16 -0600
Subject: [PATCH 02/22] fix: use proper count tokens endpoint
---
src/AnthropicClient/AnthropicApiClient.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index 831161d..1cc6ebb 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -71,7 +71,7 @@ public class AnthropicApiClient : IAnthropicApiClient
private const string BaseUrl = "https://api.anthropic.com/v1/";
private const string ApiKeyHeader = "x-api-key";
private const string MessagesEndpoint = "messages";
- private string CountTokensEndpoint => $"{MessagesEndpoint}/count-tokens";
+ private string CountTokensEndpoint => $"{MessagesEndpoint}/count_tokens";
private string MessageBatchesEndpoint => $"{MessagesEndpoint}/batches";
private const string ModelsEndpoint = "models";
private const string JsonContentType = "application/json";
From 15f5ad49e7f41215c3b089cdfe3873182223ce2d Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 8 Jan 2025 22:32:36 -0600
Subject: [PATCH 03/22] fix: make correct properties nullable
---
src/AnthropicClient/Models/MessageBatchResponse.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/AnthropicClient/Models/MessageBatchResponse.cs b/src/AnthropicClient/Models/MessageBatchResponse.cs
index b3c0a0e..925e713 100644
--- a/src/AnthropicClient/Models/MessageBatchResponse.cs
+++ b/src/AnthropicClient/Models/MessageBatchResponse.cs
@@ -33,7 +33,7 @@ public class MessageBatchResponse
/// Gets the date and time when the batch ended.
///
[JsonPropertyName("ended_at")]
- public DateTimeOffset EndedAt { get; init; }
+ public DateTimeOffset? EndedAt { get; init; }
///
/// Gets the date and time when the batch was created.
@@ -51,19 +51,19 @@ public class MessageBatchResponse
/// Gets the date and time when the batch was archived.
///
[JsonPropertyName("archived_at")]
- public DateTimeOffset ArchivedAt { get; init; }
+ public DateTimeOffset? ArchivedAt { get; init; }
///
/// Gets the date and time when the batch cancellation was initiated.
///
[JsonPropertyName("cancel_initiated_at")]
- public DateTimeOffset CancelInitiatedAt { get; init; }
+ public DateTimeOffset? CancelInitiatedAt { get; init; }
///
/// Gets the URL to the results of the batch.
///
[JsonPropertyName("results_url")]
- public string ResultsUrl { get; init; } = string.Empty;
+ public string? ResultsUrl { get; init; } = string.Empty;
}
///
From 854e4642abdb56af91484111ea0f36398f5e37c1 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 8 Jan 2025 22:33:09 -0600
Subject: [PATCH 04/22] =?UTF-8?q?tests:=20add=20integration=20tests=20for?=
=?UTF-8?q?=20remaining=20=F0=9F=A5=B2=20and=20=F0=9F=98=80=20paths?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Integration/AnthropicApiClientTests.cs | 66 +++++++++++++++++++
1 file changed, 66 insertions(+)
diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
index ca97570..ba2a6a2 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -1032,4 +1032,70 @@ public class AnthropicApiClientTests : IntegrationTest
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",
+ @"{}"
+ );
+
+ 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",
+ @"{}"
+ );
+
+ 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());
+ }
}
\ No newline at end of file
From 779a5ca2818e79081cbd9a8fab4a7cbb7b4e0222 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 8 Jan 2025 22:33:27 -0600
Subject: [PATCH 05/22] tests: add end-to-end test for creating a message batch
---
.../EndToEnd/AnthropicApiClientTests.cs | 20 +++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
index 47ccadd..2fff56f 100644
--- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
@@ -341,4 +341,24 @@ 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();
+ }
}
\ No newline at end of file
From f8e01bf4878e07f823d524408ed59a2232960a7b Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 8 Jan 2025 22:46:17 -0600
Subject: [PATCH 06/22] feat: add class for representing batch statuses
---
.../Models/MessageBatchStatus.cs | 22 +++++++++++++++++++
.../Unit/Models/MessageBatchStatusTests.cs | 22 +++++++++++++++++++
2 files changed, 44 insertions(+)
create mode 100644 src/AnthropicClient/Models/MessageBatchStatus.cs
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/MessageBatchStatusTests.cs
diff --git a/src/AnthropicClient/Models/MessageBatchStatus.cs b/src/AnthropicClient/Models/MessageBatchStatus.cs
new file mode 100644
index 0000000..5e1f299
--- /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/tests/AnthropicClient.Tests/Unit/Models/MessageBatchStatusTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchStatusTests.cs
new file mode 100644
index 0000000..1d66d45
--- /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
From c5f3c5eca549a89ac6b8197b833767c0ca4465c5 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 8 Jan 2025 23:15:24 -0600
Subject: [PATCH 07/22] feat: implement GetMessageBatchAsync method
---
src/AnthropicClient/AnthropicApiClient.cs | 26 +++-
.../EndToEnd/AnthropicApiClientTests.cs | 21 +++
.../Integration/AnthropicApiClientTests.cs | 120 ++++++++++++++++++
.../Integration/IntegrationTest.cs | 6 +
4 files changed, 172 insertions(+), 1 deletion(-)
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index 1cc6ebb..9d97391 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -34,7 +34,14 @@ public interface IAnthropicApiClient
/// 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);
+
///
/// Counts the tokens in a message asynchronously.
///
@@ -312,6 +319,23 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult.Success(msgBatchResponse, anthropicHeaders);
}
+ ///
+ public async Task> GetMessageBatchAsync(string batchId)
+ {
+ var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}");
+ 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 msgBatchResponse = Deserialize(responseContent) ?? new MessageBatchResponse();
+ return AnthropicResult.Success(msgBatchResponse, anthropicHeaders);
+ }
+
///
public async Task> CountMessageTokensAsync(CountMessageTokensRequest request)
{
diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
index 2fff56f..4909936 100644
--- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
@@ -361,4 +361,25 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
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);
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
index ba2a6a2..d450495 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -1098,4 +1098,124 @@ public class AnthropicApiClientTests : IntegrationTest
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().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 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",
+ @"{}"
+ );
+
+ 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",
+ @"{}"
+ );
+
+ var result = await Client.GetMessageBatchAsync(batchId);
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().BeOfType();
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
index 1a9ef1c..b69a60d 100644
--- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
+++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
@@ -71,4 +71,10 @@ public static class MockHttpMessageHandlerExtensions
return mockHttpMessageHandler
.SetupBaseRequest(HttpMethod.Post, MessageBatchesEndpoint);
}
+
+ public static MockedRequest WhenGetMessageBatchRequest(this MockHttpMessageHandler mockHttpMessageHandler, string batchId)
+ {
+ return mockHttpMessageHandler
+ .SetupBaseRequest(HttpMethod.Get, $"{MessageBatchesEndpoint}/{batchId}");
+ }
}
\ No newline at end of file
From 0a6d89bd77e165a0ae63dacd9f854bd49ba817ce Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 8 Jan 2025 23:20:21 -0600
Subject: [PATCH 08/22] fix: default nullable properties to actually
datetimeoffset min value when new'd up
---
src/AnthropicClient/Models/MessageBatchResponse.cs | 8 ++++----
.../Unit/Models/MessageBatchResponseTests.cs | 4 ++--
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/AnthropicClient/Models/MessageBatchResponse.cs b/src/AnthropicClient/Models/MessageBatchResponse.cs
index 925e713..2df3b43 100644
--- a/src/AnthropicClient/Models/MessageBatchResponse.cs
+++ b/src/AnthropicClient/Models/MessageBatchResponse.cs
@@ -27,13 +27,13 @@ public class MessageBatchResponse
/// Gets the counts of requests in the batch.
///
[JsonPropertyName("request_counts")]
- public MessageBatchRequestCounts RequestCounts { get; init; } = new MessageBatchRequestCounts();
+ public MessageBatchRequestCounts RequestCounts { get; init; } = new();
///
/// Gets the date and time when the batch ended.
///
[JsonPropertyName("ended_at")]
- public DateTimeOffset? EndedAt { get; init; }
+ public DateTimeOffset? EndedAt { get; init; } = DateTimeOffset.MinValue;
///
/// Gets the date and time when the batch was created.
@@ -51,13 +51,13 @@ public class MessageBatchResponse
/// Gets the date and time when the batch was archived.
///
[JsonPropertyName("archived_at")]
- public DateTimeOffset? ArchivedAt { get; init; }
+ 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; }
+ public DateTimeOffset? CancelInitiatedAt { get; init; } = DateTimeOffset.MinValue;
///
/// Gets the URL to the results of the batch.
diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResponseTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResponseTests.cs
index 4cde6a1..5001b4d 100644
--- a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResponseTests.cs
+++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResponseTests.cs
@@ -50,7 +50,7 @@ public class MessageBatchResponseTests : SerializationTest
Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF",
Type = "message_batch",
ProcessingStatus = "in_progress",
- RequestCounts = new MessageBatchRequestCounts
+ RequestCounts = new()
{
Processing = 100,
Succeeded = 50,
@@ -94,7 +94,7 @@ public class MessageBatchResponseTests : SerializationTest
Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF",
Type = "message_batch",
ProcessingStatus = "in_progress",
- RequestCounts = new MessageBatchRequestCounts
+ RequestCounts = new()
{
Processing = 100,
Succeeded = 50,
From c56adf394c62aca10c89f1d8650635cefe248d50 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Thu, 9 Jan 2025 23:17:41 -0600
Subject: [PATCH 09/22] feat: implement GetMessageBatchResultsAsync method
---
src/AnthropicClient/AnthropicApiClient.cs | 47 +++++++++++++++++--
.../Json/JsonSerializationOptions.cs | 1 +
.../Json/MessageBatchResultConverter.cs | 29 ++++++++++++
.../Models/CanceledMessageBatchResult.cs | 14 ++++++
.../Models/ErroredMessageBatchResult.cs | 19 ++++++++
.../Models/ExpiredMessageBatchResult.cs | 14 ++++++
.../Models/MessageBatchResult.cs | 26 ++++++++++
.../Models/MessageBatchResultItem.cs | 20 ++++++++
.../Models/MessageBatchResultType.cs | 27 +++++++++++
.../Models/SucceededMessageBatchResult.cs | 20 ++++++++
.../EndToEnd/AnthropicApiClientTests.cs | 21 ++++-----
.../Files/TestFileHelper.cs | 7 +++
.../Files/batch_results.jsonl | 5 ++
.../Integration/AnthropicApiClientTests.cs | 41 +++++++++++++---
.../Integration/IntegrationTest.cs | 12 ++++-
.../Models/MessageBatchResultItemTests.cs | 14 ++++++
16 files changed, 294 insertions(+), 23 deletions(-)
create mode 100644 src/AnthropicClient/Json/MessageBatchResultConverter.cs
create mode 100644 src/AnthropicClient/Models/CanceledMessageBatchResult.cs
create mode 100644 src/AnthropicClient/Models/ErroredMessageBatchResult.cs
create mode 100644 src/AnthropicClient/Models/ExpiredMessageBatchResult.cs
create mode 100644 src/AnthropicClient/Models/MessageBatchResult.cs
create mode 100644 src/AnthropicClient/Models/MessageBatchResultItem.cs
create mode 100644 src/AnthropicClient/Models/MessageBatchResultType.cs
create mode 100644 src/AnthropicClient/Models/SucceededMessageBatchResult.cs
create mode 100644 tests/AnthropicClient.Tests/Files/TestFileHelper.cs
create mode 100644 tests/AnthropicClient.Tests/Files/batch_results.jsonl
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultItemTests.cs
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index 9d97391..cb5f0c5 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -34,14 +34,21 @@ public interface IAnthropicApiClient
/// 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);
-
+
+ ///
+ /// 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.
///
@@ -325,17 +332,49 @@ public class AnthropicApiClient : IAnthropicApiClient
var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}");
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 msgBatchResponse = Deserialize(responseContent) ?? new MessageBatchResponse();
return AnthropicResult.Success(msgBatchResponse, anthropicHeaders);
}
+ ///
+ public async Task>> GetMessageBatchResultsAsync(string batchId)
+ {
+ var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}/results");
+ var anthropicHeaders = new AnthropicHeaders(response.Headers);
+
+ if (response.IsSuccessStatusCode is false)
+ {
+ var content = await response.Content.ReadAsStringAsync();
+ var error = Deserialize(content) ?? new AnthropicError();
+ return AnthropicResult>.Failure(error, anthropicHeaders);
+ }
+
+ async IAsyncEnumerable ReadResults()
+ {
+ 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();
+ }
+ }
+
+ return AnthropicResult>.Success(ReadResults(), anthropicHeaders);
+ }
+
///
public async Task> CountMessageTokensAsync(CountMessageTokensRequest request)
{
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..878bff6
--- /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)
+ {
+ }
+}
diff --git a/src/AnthropicClient/Models/ErroredMessageBatchResult.cs b/src/AnthropicClient/Models/ErroredMessageBatchResult.cs
new file mode 100644
index 0000000..4e7dbf5
--- /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)
+ {
+ }
+}
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/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..14f9be6
--- /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!;
+}
diff --git a/src/AnthropicClient/Models/MessageBatchResultType.cs b/src/AnthropicClient/Models/MessageBatchResultType.cs
new file mode 100644
index 0000000..187c2c2
--- /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";
+}
diff --git a/src/AnthropicClient/Models/SucceededMessageBatchResult.cs b/src/AnthropicClient/Models/SucceededMessageBatchResult.cs
new file mode 100644
index 0000000..82260e8
--- /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)
+ {
+ }
+}
diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
index 4909936..ad4c60b 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)
{
- 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);
@@ -354,9 +353,9 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
)
),
]);
-
+
var result = await _client.CreateMessageBatchAsync(request);
-
+
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType();
result.Value.Id.Should().NotBeNullOrEmpty();
@@ -374,7 +373,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
)
),
]);
-
+
var createResult = await _client.CreateMessageBatchAsync(request);
var getResult = await _client.GetMessageBatchAsync(createResult.Value.Id);
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 d450495..f3fa91e 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -1,3 +1,6 @@
+using AnthropicClient.Tests.Files;
+using AnthropicClient.Tests.Unit;
+
namespace AnthropicClient.Tests.Integration;
public class AnthropicApiClientTests : IntegrationTest
@@ -1032,7 +1035,7 @@ public class AnthropicApiClientTests : IntegrationTest
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()
{
@@ -1058,7 +1061,7 @@ public class AnthropicApiClientTests : IntegrationTest
result.Error.Should().BeOfType();
result.Error.Error.Should().BeOfType();
}
-
+
[Fact]
public async Task CreateMessageBatchAsync_WhenCalledRequestFailsAndErrorCanNotBeDeserialized_ItShouldReturnUnknownError()
{
@@ -1078,7 +1081,7 @@ public class AnthropicApiClientTests : IntegrationTest
result.Error.Should().BeOfType();
result.Error.Error.Should().BeOfType();
}
-
+
[Fact]
public async Task CreateMessageBatchAsync_WhenCalledRequestSucceedsAndCanNotDeserializeResponse_ItShouldReturnEmptyResponse()
{
@@ -1153,7 +1156,7 @@ public class AnthropicApiClientTests : IntegrationTest
result.Value.ResultsUrl.Should()
.Be("https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results");
}
-
+
[Fact]
public async Task GetMessageBatchAsync_WhenCalledAndErrorReturned_ItShouldHandleError()
{
@@ -1179,7 +1182,7 @@ public class AnthropicApiClientTests : IntegrationTest
result.Error.Should().BeOfType();
result.Error.Error.Should().BeOfType();
}
-
+
[Fact]
public async Task GetMessageBatchAsync_WhenCalledRequestFailsAndErrorCanNotBeDeserialized_ItShouldReturnUnknownError()
{
@@ -1199,7 +1202,7 @@ public class AnthropicApiClientTests : IntegrationTest
result.Error.Should().BeOfType();
result.Error.Error.Should().BeOfType();
}
-
+
[Fact]
public async Task GetMessageBatchAsync_WhenCalledAndCanNotDeserializeResponse_ItShouldReturnEmptyResponse()
{
@@ -1218,4 +1221,30 @@ public class AnthropicApiClientTests : IntegrationTest
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);
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
index b69a60d..90653d4 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();
@@ -71,10 +73,16 @@ public static class MockHttpMessageHandlerExtensions
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");
+ }
}
\ 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
From 3034544ca43cbb248b0f6ab4c4451c3ff4320046 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Thu, 9 Jan 2025 23:40:55 -0600
Subject: [PATCH 10/22] refactor: move local function below return
---
src/AnthropicClient/AnthropicApiClient.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index cb5f0c5..fbe11c6 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -356,7 +356,9 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult>.Failure(error, anthropicHeaders);
}
- async IAsyncEnumerable ReadResults()
+ return AnthropicResult>.Success(ReadResultsAsync(), anthropicHeaders);
+
+ async IAsyncEnumerable ReadResultsAsync()
{
using var responseContent = await response.Content.ReadAsStreamAsync();
using var streamReader = new StreamReader(responseContent);
@@ -371,8 +373,6 @@ public class AnthropicApiClient : IAnthropicApiClient
line = await streamReader.ReadLineAsync();
}
}
-
- return AnthropicResult>.Success(ReadResults(), anthropicHeaders);
}
///
From 299af3b759e116c77511baf56126565c4654f5d0 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Fri, 10 Jan 2025 22:52:46 -0600
Subject: [PATCH 11/22] tests: add test for batch result type and succeeded
batch result model
---
.../Models/MessageBatchResultTypeTests.cs | 36 +++++++
.../SucceededMessageBatchResultTests.cs | 95 +++++++++++++++++++
2 files changed, 131 insertions(+)
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTypeTests.cs
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/SucceededMessageBatchResultTests.cs
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/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
From 876c6f571c99082c401259d35fa0e30bac94a328 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Sun, 12 Jan 2025 13:26:31 -0600
Subject: [PATCH 12/22] tests: cover null responses
---
src/AnthropicClient/AnthropicApiClient.cs | 1 -
.../Json/ContentDeltaConverter.cs | 1 -
.../Json/EventDataConverter.cs | 1 -
.../Integration/AnthropicApiClientTests.cs | 239 +++++++++++++++++-
.../Models/CanceledMessageBatchResultTests.cs | 34 +++
.../Unit/Models/ErroredMessageBatchResult.cs | 48 ++++
.../Models/ExpiredMessageBatchResultTests.cs | 34 +++
.../Unit/Models/MessageBatchResultTests.cs | 25 ++
.../Unit/Models/ToolCallTests.cs | 2 -
.../Unit/SerializationTest.cs | 2 -
10 files changed, 370 insertions(+), 17 deletions(-)
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/CanceledMessageBatchResultTests.cs
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/ErroredMessageBatchResult.cs
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/ExpiredMessageBatchResultTests.cs
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultTests.cs
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index fbe11c6..add0174 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;
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/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
index f3fa91e..e5a5a33 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -1,5 +1,4 @@
using AnthropicClient.Tests.Files;
-using AnthropicClient.Tests.Unit;
namespace AnthropicClient.Tests.Integration;
@@ -35,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()
{
@@ -363,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()
{
@@ -489,7 +561,7 @@ public class AnthropicApiClientTests : IntegrationTest
.Respond(
HttpStatusCode.BadRequest,
"application/json",
- @"{}"
+ @"null"
);
var request = new CountMessageTokensRequest(
@@ -507,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()
{
@@ -656,7 +753,7 @@ public class AnthropicApiClientTests : IntegrationTest
.Respond(
HttpStatusCode.BadRequest,
"application/json",
- @"{}"
+ @"null"
);
var result = await Client.ListModelsAsync();
@@ -666,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()
{
@@ -803,7 +921,7 @@ public class AnthropicApiClientTests : IntegrationTest
.Respond(
HttpStatusCode.BadRequest,
"application/json",
- @"{}"
+ @"null"
);
var responses = Client.ListAllModelsAsync();
@@ -889,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()
{
@@ -953,7 +1100,7 @@ public class AnthropicApiClientTests : IntegrationTest
.Respond(
HttpStatusCode.BadRequest,
"application/json",
- @"{}"
+ @"null"
);
var result = await Client.GetModelAsync(modelId);
@@ -973,7 +1120,7 @@ public class AnthropicApiClientTests : IntegrationTest
.Respond(
HttpStatusCode.OK,
"application/json",
- @"{}"
+ @"null"
);
var result = await Client.GetModelAsync(modelId);
@@ -1070,7 +1217,7 @@ public class AnthropicApiClientTests : IntegrationTest
.Respond(
HttpStatusCode.BadRequest,
"application/json",
- @"{}"
+ @"null"
);
var request = new MessageBatchRequest([new("custom_id", new())]);
@@ -1090,7 +1237,7 @@ public class AnthropicApiClientTests : IntegrationTest
.Respond(
HttpStatusCode.OK,
"application/json",
- @"{}"
+ @"null"
);
var request = new MessageBatchRequest([new("custom_id", new())]);
@@ -1193,7 +1340,7 @@ public class AnthropicApiClientTests : IntegrationTest
.Respond(
HttpStatusCode.BadRequest,
"application/json",
- @"{}"
+ @"null"
);
var result = await Client.GetMessageBatchAsync(batchId);
@@ -1213,7 +1360,7 @@ public class AnthropicApiClientTests : IntegrationTest
.Respond(
HttpStatusCode.OK,
"application/json",
- @"{}"
+ @"null"
);
var result = await Client.GetMessageBatchAsync(batchId);
@@ -1247,4 +1394,76 @@ public class AnthropicApiClientTests : IntegrationTest
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();
+ }
}
\ 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/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/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
From ca2ecdcfc79a5ff96c22eb564e7fc8be22e82a6f Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Sun, 12 Jan 2025 13:46:05 -0600
Subject: [PATCH 13/22] feat: implement ListMessageBatchesAsync method
---
src/AnthropicClient/AnthropicApiClient.cs | 26 ++
.../EndToEnd/AnthropicApiClientTests.cs | 24 +-
.../Integration/AnthropicApiClientTests.cs | 230 ++++++++++++++++++
.../Integration/IntegrationTest.cs | 6 +
4 files changed, 285 insertions(+), 1 deletion(-)
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index add0174..bed8011 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -41,6 +41,13 @@ public interface IAnthropicApiClient
/// 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);
+
///
/// Gets the results of a message batch asynchronously.
///
@@ -342,6 +349,25 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult.Success(msgBatchResponse, anthropicHeaders);
}
+ ///
+ public async Task>> ListMessageBatchesAsync(PagingRequest? request = null)
+ {
+ var pagingRequest = request ?? new PagingRequest();
+ var endpoint = $"{MessageBatchesEndpoint}?{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);
+ }
+
///
public async Task>> GetMessageBatchResultsAsync(string batchId)
{
diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
index ad4c60b..5cb7170 100644
--- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
@@ -2,7 +2,7 @@ using AnthropicClient.Tests.Files;
namespace AnthropicClient.Tests.EndToEnd;
-public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture)
+public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture)
{
[Fact]
public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse()
@@ -381,4 +381,26 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
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);
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
index e5a5a33..76d4bea 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -1466,4 +1466,234 @@ public class AnthropicApiClientTests : IntegrationTest
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();
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
index 90653d4..465319d 100644
--- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
+++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
@@ -85,4 +85,10 @@ public static class MockHttpMessageHandlerExtensions
return mockHttpMessageHandler
.SetupBaseRequest(HttpMethod.Get, $"{MessageBatchesEndpoint}/{batchId}/results");
}
+
+ public static MockedRequest WhenListMessageBatchesRequest(this MockHttpMessageHandler mockHttpMessageHandler)
+ {
+ return mockHttpMessageHandler
+ .SetupBaseRequest(HttpMethod.Get, MessageBatchesEndpoint);
+ }
}
\ No newline at end of file
From 180a0901959b88b27981f063b0be7a267a141802 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Mon, 13 Jan 2025 12:42:23 -0600
Subject: [PATCH 14/22] feat: add ListAllMessageBatchesAsync method and
corresponding tests
---
src/AnthropicClient/AnthropicApiClient.cs | 84 +++++++-----
.../EndToEnd/AnthropicApiClientTests.cs | 32 +++++
.../Integration/AnthropicApiClientTests.cs | 127 ++++++++++++++++++
3 files changed, 213 insertions(+), 30 deletions(-)
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index bed8011..8c0b3fb 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -48,6 +48,13 @@ public interface IAnthropicApiClient
/// 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);
+
///
/// Gets the results of a message batch asynchronously.
///
@@ -368,6 +375,15 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult>.Success(page, anthropicHeaders);
}
+ ///
+ public async IAsyncEnumerable>> ListAllMessageBatchesAsync(int limit = 20)
+ {
+ await foreach (var result in GetAllPagesAsync(MessageBatchesEndpoint, limit))
+ {
+ yield return result;
+ }
+ }
+
///
public async Task>> GetMessageBatchResultsAsync(string batchId)
{
@@ -439,37 +455,10 @@ public class AnthropicApiClient : IAnthropicApiClient
///
public async IAsyncEnumerable>> ListAllModelsAsync(int limit = 20)
{
- var pagingRequest = new PagingRequest(limit: limit);
- string Endpoint() => $"{ModelsEndpoint}?{pagingRequest.ToQueryParameters()}";
- bool hasMore;
-
- do
+ await foreach (var result in GetAllPagesAsync(ModelsEndpoint, limit))
{
- 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();
- yield return AnthropicResult>.Failure(error, anthropicHeaders);
- yield break;
- }
-
- var page = Deserialize>(responseContent) ?? new Page();
-
- if (page.HasMore && page.LastId is not null)
- {
- hasMore = true;
- pagingRequest = new PagingRequest(limit: limit, afterId: page.LastId);
- }
- else
- {
- hasMore = false;
- }
-
- yield return AnthropicResult>.Success(page, anthropicHeaders);
- } while (hasMore);
+ yield return result;
+ }
}
///
@@ -490,6 +479,41 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult.Success(model, anthropicHeaders);
}
+ private async IAsyncEnumerable>> GetAllPagesAsync(string endpoint, int limit = 20)
+ {
+ var pagingRequest = new PagingRequest(limit: limit);
+ string Endpoint() => $"{endpoint}?{pagingRequest.ToQueryParameters()}";
+ bool hasMore;
+
+ do
+ {
+ 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();
+ yield return AnthropicResult>.Failure(error, anthropicHeaders);
+ yield break;
+ }
+
+ var page = Deserialize>(responseContent) ?? new Page();
+
+ if (page.HasMore && page.LastId is not null)
+ {
+ hasMore = true;
+ pagingRequest = new PagingRequest(limit: limit, afterId: page.LastId);
+ }
+ else
+ {
+ hasMore = false;
+ }
+
+ yield return AnthropicResult>.Success(page, anthropicHeaders);
+ } while (hasMore);
+ }
+
private ToolCall? GetToolCall(MessageResponse response, List tools)
{
var toolUse = response.Content.OfType().FirstOrDefault();
diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
index 5cb7170..4cdd660 100644
--- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
@@ -403,4 +403,36 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
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);
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
index 76d4bea..2b458c2 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -1696,4 +1696,131 @@ public class AnthropicApiClientTests : IntegrationTest
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
+ }
+ });
+ }
}
\ No newline at end of file
From c11600c77040eebfab4df1153d07774357d87ce2 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Mon, 13 Jan 2025 13:11:47 -0600
Subject: [PATCH 15/22] feat: add CancelMessageBatchAsync method and
corresponding tests
---
src/AnthropicClient/AnthropicApiClient.cs | 30 ++++-
.../Integration/AnthropicApiClientTests.cs | 117 +++++++++++++++---
.../Integration/IntegrationTest.cs | 6 +
3 files changed, 133 insertions(+), 20 deletions(-)
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index 8c0b3fb..34244c3 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -55,6 +55,13 @@ public interface IAnthropicApiClient
/// 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);
+
///
/// Gets the results of a message batch asynchronously.
///
@@ -384,6 +391,24 @@ public class AnthropicApiClient : IAnthropicApiClient
}
}
+ ///
+ public async Task> CancelMessageBatchAsync(string batchId)
+ {
+ var endpoint = $"{MessageBatchesEndpoint}/{batchId}/cancel";
+ var response = await SendRequestAsync(endpoint, HttpMethod.Post);
+ 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 msgBatchResponse = Deserialize(responseContent) ?? new MessageBatchResponse();
+ return AnthropicResult.Success(msgBatchResponse, anthropicHeaders);
+ }
+
///
public async Task>> GetMessageBatchResultsAsync(string batchId)
{
@@ -533,9 +558,10 @@ public class AnthropicApiClient : IAnthropicApiClient
return new ToolCall(tool, toolUse);
}
- private async Task SendRequestAsync(string endpoint)
+ private async Task SendRequestAsync(string endpoint, HttpMethod? method = null)
{
- return await _httpClient.GetAsync(endpoint);
+ var request = new HttpRequestMessage(method ?? HttpMethod.Get, endpoint);
+ return await _httpClient.SendAsync(request);
}
private async Task SendRequestAsync(string endpoint, T request)
diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
index 2b458c2..8b312c2 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -1282,26 +1282,26 @@ public class AnthropicApiClientTests : IntegrationTest
var result = await Client.GetMessageBatchAsync(batchId);
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
+ result.Value.Should().BeEquivalentTo(new MessageBatchResponse()
{
- Processing = 100,
- Succeeded = 50,
- Errored = 30,
- Canceled = 10,
- Expired = 10
+ 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"
});
-
- 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]
@@ -1823,4 +1823,85 @@ public class AnthropicApiClientTests : IntegrationTest
}
});
}
+
+ [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();
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
index 465319d..e521d0c 100644
--- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
+++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
@@ -91,4 +91,10 @@ public static class MockHttpMessageHandlerExtensions
return mockHttpMessageHandler
.SetupBaseRequest(HttpMethod.Get, MessageBatchesEndpoint);
}
+
+ public static MockedRequest WhenCancelMessageBatchRequest(this MockHttpMessageHandler mockHttpMessageHandler, string batchId)
+ {
+ return mockHttpMessageHandler
+ .SetupBaseRequest(HttpMethod.Post, $"{MessageBatchesEndpoint}/{batchId}/cancel");
+ }
}
\ No newline at end of file
From a3ac1001f65f6a1ddfc5c285b497f5122e4ff19e Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Tue, 14 Jan 2025 12:57:52 -0600
Subject: [PATCH 16/22] tests: add end to end test for canceling a batch
---
.../EndToEnd/AnthropicApiClientTests.cs | 22 +++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
index 4cdd660..8fa81f6 100644
--- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
@@ -435,4 +435,26 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
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
From 7ec931dda53678899713cc92fad0b068ae622313 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Tue, 14 Jan 2025 13:14:14 -0600
Subject: [PATCH 17/22] refactor: add private method to dry class up a bit
---
src/AnthropicClient/AnthropicApiClient.cs | 99 +++++------------------
1 file changed, 22 insertions(+), 77 deletions(-)
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index 34244c3..13ceafe 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -333,34 +333,14 @@ public class AnthropicApiClient : IAnthropicApiClient
public async Task> CreateMessageBatchAsync(MessageBatchRequest request)
{
var response = await SendRequestAsync(MessageBatchesEndpoint, request);
- var anthropicHeaders = new AnthropicHeaders(response.Headers);
- var responseContent = await response.Content.ReadAsStringAsync();
-
- if (response.IsSuccessStatusCode is false)
- {
- var error = Deserialize(responseContent) ?? new AnthropicError();
- return AnthropicResult.Failure(error, anthropicHeaders);
- }
-
- var msgBatchResponse = Deserialize(responseContent) ?? new MessageBatchResponse();
- return AnthropicResult.Success(msgBatchResponse, anthropicHeaders);
+ return await CreateResultAsync(response);
}
///
public async Task> GetMessageBatchAsync(string batchId)
{
var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}");
- 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 msgBatchResponse = Deserialize(responseContent) ?? new MessageBatchResponse();
- return AnthropicResult.Success(msgBatchResponse, anthropicHeaders);
+ return await CreateResultAsync(response);
}
///
@@ -369,17 +349,7 @@ public class AnthropicApiClient : IAnthropicApiClient
var pagingRequest = request ?? new PagingRequest();
var endpoint = $"{MessageBatchesEndpoint}?{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);
}
///
@@ -396,17 +366,7 @@ public class AnthropicApiClient : IAnthropicApiClient
{
var endpoint = $"{MessageBatchesEndpoint}/{batchId}/cancel";
var response = await SendRequestAsync(endpoint, HttpMethod.Post);
- 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 msgBatchResponse = Deserialize(responseContent) ?? new MessageBatchResponse();
- return AnthropicResult.Success(msgBatchResponse, anthropicHeaders);
+ return await CreateResultAsync(response);
}
///
@@ -445,17 +405,7 @@ public class AnthropicApiClient : IAnthropicApiClient
public async Task> CountMessageTokensAsync(CountMessageTokensRequest request)
{
var response = await SendRequestAsync(CountTokensEndpoint, request);
- var anthropicHeaders = new AnthropicHeaders(response.Headers);
- var responseContent = await response.Content.ReadAsStringAsync();
-
- if (response.IsSuccessStatusCode is false)
- {
- var error = Deserialize(responseContent) ?? new AnthropicError();
- return AnthropicResult.Failure(error, anthropicHeaders);
- }
-
- var msgResponse = Deserialize(responseContent) ?? new TokenCountResponse();
- return AnthropicResult.Success(msgResponse, anthropicHeaders);
+ return await CreateResultAsync(response);
}
///
@@ -464,17 +414,7 @@ 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);
}
///
@@ -491,17 +431,7 @@ public class AnthropicApiClient : IAnthropicApiClient
{
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);
+ return await CreateResultAsync(response);
}
private async IAsyncEnumerable>> GetAllPagesAsync(string endpoint, int limit = 20)
@@ -558,6 +488,21 @@ public class AnthropicApiClient : IAnthropicApiClient
return new ToolCall(tool, toolUse);
}
+ private async Task> CreateResultAsync(HttpResponseMessage response) where T : new()
+ {
+ var anthropicHeaders = new AnthropicHeaders(response.Headers);
+ var responseContent = await response.Content.ReadAsStringAsync();
+
+ if (response.IsSuccessStatusCode is false)
+ {
+ var error = Deserialize(responseContent) ?? new AnthropicError();
+ return AnthropicResult.Failure(error, anthropicHeaders);
+ }
+
+ var model = Deserialize(responseContent) ?? new T();
+ return AnthropicResult.Success(model, anthropicHeaders);
+ }
+
private async Task SendRequestAsync(string endpoint, HttpMethod? method = null)
{
var request = new HttpRequestMessage(method ?? HttpMethod.Get, endpoint);
From 56b3a53a2a31070693351525379b6a675b068496 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Tue, 14 Jan 2025 13:41:32 -0600
Subject: [PATCH 18/22] feat: add DeleteMessageBatchAsync method and
MessageBatchDeleteResponse model with tests
---
src/AnthropicClient/AnthropicApiClient.cs | 99 ++---------------
src/AnthropicClient/IAnthropicApiClient.cs | 101 ++++++++++++++++++
.../Models/MessageBatchDeleteResponse.cs | 17 +++
.../Integration/AnthropicApiClientTests.cs | 24 +++++
.../Integration/IntegrationTest.cs | 6 ++
.../Models/MessageBatchDeleteResponseTests.cs | 41 +++++++
6 files changed, 197 insertions(+), 91 deletions(-)
create mode 100644 src/AnthropicClient/IAnthropicApiClient.cs
create mode 100644 src/AnthropicClient/Models/MessageBatchDeleteResponse.cs
create mode 100644 tests/AnthropicClient.Tests/Unit/Models/MessageBatchDeleteResponseTests.cs
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index 13ceafe..826b735 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -8,97 +8,6 @@ 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);
-
- ///
- /// 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);
-
- ///
- /// 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);
-}
-
///
public class AnthropicApiClient : IAnthropicApiClient
{
@@ -369,6 +278,14 @@ public class AnthropicApiClient : IAnthropicApiClient
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)
{
diff --git a/src/AnthropicClient/IAnthropicApiClient.cs b/src/AnthropicClient/IAnthropicApiClient.cs
new file mode 100644
index 0000000..8081166
--- /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);
+}
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/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
index 8b312c2..6cab022 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -1904,4 +1904,28 @@ public class AnthropicApiClientTests : IntegrationTest
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 e521d0c..223107b 100644
--- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
+++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
@@ -97,4 +97,10 @@ public static class MockHttpMessageHandlerExtensions
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/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
From 2794c52c2a411359381b84c90be056aa7b4c2f68 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Tue, 14 Jan 2025 13:52:46 -0600
Subject: [PATCH 19/22] docs: stub out batch documentation
---
README.md | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/README.md b/README.md
index dc0901f..293aebc 100644
--- a/README.md
+++ b/README.md
@@ -971,3 +971,37 @@ 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
+
+```csharp
+```
+
+#### Get a message batch
+
+```csharp
+```
+
+#### Get a message batch results
+
+```csharp
+```
+
+#### List message batches
+
+```csharp
+```
+
+#### Cancel a message batch
+
+```csharp
+```
+
+#### Delete a message batch
+
+```csharp
+```
From 07cadf17af554c251e3a8bd49f0d31ab6c0b7c7c Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 15 Jan 2025 10:23:59 -0600
Subject: [PATCH 20/22] docs: add examples for message batch api methods
---
README.md | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 156 insertions(+)
diff --git a/README.md b/README.md
index 293aebc..696342b 100644
--- a/README.md
+++ b/README.md
@@ -978,30 +978,186 @@ Anthropic provides a feature called [Message Batches](https://docs.anthropic.com
#### 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 = await 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);
```
From ee9dbcf647cdb72b13fc6964035b766fcd2a2898 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 15 Jan 2025 10:29:23 -0600
Subject: [PATCH 21/22] docs: fix example
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 696342b..f23653a 100644
--- a/README.md
+++ b/README.md
@@ -1100,7 +1100,7 @@ You can also retrieve all the pages of message batches without having to impleme
using AnthropicClient;
using AnthropicClient.Models;
-var pageResponses = await client.ListAllMessageBatchesAsync();
+var pageResponses = client.ListAllMessageBatchesAsync();
await foreach (var response in pageResponses)
{
From fc2932f3c51f28e89ddf11416cd5e707fee902a6 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 15 Jan 2025 10:30:02 -0600
Subject: [PATCH 22/22] chore: run dotnet format
---
src/AnthropicClient/IAnthropicApiClient.cs | 2 +-
src/AnthropicClient/Models/CanceledMessageBatchResult.cs | 2 +-
src/AnthropicClient/Models/ErroredMessageBatchResult.cs | 2 +-
src/AnthropicClient/Models/MessageBatchResultItem.cs | 2 +-
src/AnthropicClient/Models/MessageBatchResultType.cs | 2 +-
src/AnthropicClient/Models/MessageBatchStatus.cs | 4 ++--
src/AnthropicClient/Models/SucceededMessageBatchResult.cs | 2 +-
.../Unit/Models/MessageBatchStatusTests.cs | 4 ++--
8 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/AnthropicClient/IAnthropicApiClient.cs b/src/AnthropicClient/IAnthropicApiClient.cs
index 8081166..27c7e00 100644
--- a/src/AnthropicClient/IAnthropicApiClient.cs
+++ b/src/AnthropicClient/IAnthropicApiClient.cs
@@ -98,4 +98,4 @@ public interface IAnthropicApiClient
/// 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/Models/CanceledMessageBatchResult.cs b/src/AnthropicClient/Models/CanceledMessageBatchResult.cs
index 878bff6..e2f02f6 100644
--- a/src/AnthropicClient/Models/CanceledMessageBatchResult.cs
+++ b/src/AnthropicClient/Models/CanceledMessageBatchResult.cs
@@ -11,4 +11,4 @@ public class CanceledMessageBatchResult : MessageBatchResult
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
index 4e7dbf5..467dc7e 100644
--- a/src/AnthropicClient/Models/ErroredMessageBatchResult.cs
+++ b/src/AnthropicClient/Models/ErroredMessageBatchResult.cs
@@ -16,4 +16,4 @@ public class ErroredMessageBatchResult : MessageBatchResult
public ErroredMessageBatchResult() : base(MessageBatchResultType.Errored)
{
}
-}
+}
\ No newline at end of file
diff --git a/src/AnthropicClient/Models/MessageBatchResultItem.cs b/src/AnthropicClient/Models/MessageBatchResultItem.cs
index 14f9be6..5613619 100644
--- a/src/AnthropicClient/Models/MessageBatchResultItem.cs
+++ b/src/AnthropicClient/Models/MessageBatchResultItem.cs
@@ -17,4 +17,4 @@ public class MessageBatchResultItem
/// 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
index 187c2c2..6f14964 100644
--- a/src/AnthropicClient/Models/MessageBatchResultType.cs
+++ b/src/AnthropicClient/Models/MessageBatchResultType.cs
@@ -24,4 +24,4 @@ public static class MessageBatchResultType
/// 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
index 5e1f299..6730674 100644
--- a/src/AnthropicClient/Models/MessageBatchStatus.cs
+++ b/src/AnthropicClient/Models/MessageBatchStatus.cs
@@ -9,12 +9,12 @@ 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.
///
diff --git a/src/AnthropicClient/Models/SucceededMessageBatchResult.cs b/src/AnthropicClient/Models/SucceededMessageBatchResult.cs
index 82260e8..de8bcea 100644
--- a/src/AnthropicClient/Models/SucceededMessageBatchResult.cs
+++ b/src/AnthropicClient/Models/SucceededMessageBatchResult.cs
@@ -17,4 +17,4 @@ public class SucceededMessageBatchResult : MessageBatchResult
public SucceededMessageBatchResult() : base(MessageBatchResultType.Succeeded)
{
}
-}
+}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchStatusTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchStatusTests.cs
index 1d66d45..316a403 100644
--- a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchStatusTests.cs
+++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchStatusTests.cs
@@ -7,13 +7,13 @@ public class MessageBatchStatusTests
{
MessageBatchStatus.Canceling.Should().Be("canceling");
}
-
+
[Fact]
public void InProgress_WhenCalled_ItShouldReturnCancelingStatus()
{
MessageBatchStatus.InProgress.Should().Be("in_progress");
}
-
+
[Fact]
public void Ended_WhenCalled_ItShouldReturnCancelingStatus()
{