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