using AnthropicClient.Tests.Files; namespace AnthropicClient.Tests.Integration; public class AnthropicApiClientTests : IntegrationTest { [Theory] [ClassData(typeof(ErrorTestData))] public async Task CreateMessageAsync_WhenCalledAndErrorReturned_ItShouldHandleError( HttpStatusCode statusCode, string content, Type errorType ) { _mockHttpMessageHandler .WhenCreateMessageRequest() .Respond( statusCode, "application/json", content ); 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(); var actualErrorType = result.Error.Error!.GetType(); 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() { _mockHttpMessageHandler .WhenCreateMessageRequest() .Respond( HttpStatusCode.OK, "application/json", @"{ ""content"": [ { ""text"": ""Hi! My name is Claude."", ""type"": ""text"" } ], ""id"": ""msg_013Zva2CMHLNnXjNJJKqJ2EF"", ""model"": ""claude-3-5-sonnet-20240620"", ""role"": ""assistant"", ""stop_reason"": ""end_turn"", ""stop_sequence"": null, ""type"": ""message"", ""usage"": { ""input_tokens"": 10, ""output_tokens"": 25 } }" ); var request = new MessageRequest( model: AnthropicModels.Claude35Sonnet, messages: [new(MessageRole.User, [new TextContent("Hello!")])] ); var result = await Client.CreateMessageAsync(request); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); var message = result.Value; message.Id.Should().Be("msg_013Zva2CMHLNnXjNJJKqJ2EF"); message.Model.Should().Be("claude-3-5-sonnet-20240620"); message.Role.Should().Be("assistant"); message.StopReason.Should().Be("end_turn"); message.StopSequence.Should().BeNull(); message.Type.Should().Be("message"); message.Usage.InputTokens.Should().Be(10); message.Usage.OutputTokens.Should().Be(25); message.Content.Should().HaveCount(1); message.ToolCall.Should().BeNull(); var textContent = message.Content[0]; textContent.Should().BeOfType(); textContent.As().Text.Should().Be("Hi! My name is Claude."); textContent.As().Type.Should().Be("text"); } [Fact] public async Task CreateMessageAsync_WhenCalledAndMessageCreatedWithToolUseContentAndToolIsProvided_ItShouldReturnMessageWithToolCall() { _mockHttpMessageHandler .WhenCreateMessageRequest() .Respond( HttpStatusCode.OK, "application/json", @"{ ""content"": [ { ""id"": ""toolu_01D7FLrfh4GYq7yT1ULFeyMV"", ""name"": ""get_stock_price"", ""input"": { ""ticker"": ""^GSPC"" }, ""type"": ""tool_use"" } ], ""id"": ""msg_01D7FLrfh4GYq7yT1ULFeyMV"", ""model"": ""claude-3-5-sonnet-20240620"", ""role"": ""assistant"", ""stop_reason"": ""end_turn"", ""stop_sequence"": null, ""type"": ""message"", ""usage"": { ""input_tokens"": 10, ""output_tokens"": 25 } }" ); var func = (string ticker) => ticker; var request = new MessageRequest( model: AnthropicModels.Claude35Sonnet, messages: [ new(MessageRole.User, [new TextContent("Hello!")]), ], tools: [ Tool.CreateFromFunction("get_stock_price", "Gets the stock price for a given ticker", func) ] ); var result = await Client.CreateMessageAsync(request); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); var message = result.Value; message.Id.Should().Be("msg_01D7FLrfh4GYq7yT1ULFeyMV"); message.Model.Should().Be("claude-3-5-sonnet-20240620"); message.Role.Should().Be("assistant"); message.StopReason.Should().Be("end_turn"); message.StopSequence.Should().BeNull(); message.Type.Should().Be("message"); message.Usage.InputTokens.Should().Be(10); message.Usage.OutputTokens.Should().Be(25); message.Content.Should().HaveCount(1); message.ToolCall.Should().NotBeNull(); var toolUseContent = message.Content[0]; toolUseContent.Should().BeOfType(); toolUseContent.As().Id.Should().Be("toolu_01D7FLrfh4GYq7yT1ULFeyMV"); toolUseContent.As().Name.Should().Be("get_stock_price"); toolUseContent.As().Type.Should().Be("tool_use"); var input = toolUseContent.As().Input; var ticker = input.GetValueOrDefault("ticker"); ticker!.ToString().Should().Be("^GSPC"); var toolCallResult = await message.ToolCall!.InvokeAsync(); toolCallResult.IsSuccess.Should().BeTrue(); toolCallResult.Value!.ToString().Should().Be("^GSPC"); } [Fact] public async Task CreateMessageAsync_WhenCalledAndMessageCreatedWithToolUseButNoToolProvided_ItShouldReturnMessageWithoutToolCall() { _mockHttpMessageHandler .WhenCreateMessageRequest() .Respond( HttpStatusCode.OK, "application/json", @"{ ""content"": [ { ""id"": ""toolu_01D7FLrfh4GYq7yT1ULFeyMV"", ""name"": ""get_stock_price"", ""input"": { ""ticker"": ""^GSPC"" }, ""type"": ""tool_use"" } ], ""id"": ""msg_01D7FLrfh4GYq7yT1ULFeyMV"", ""model"": ""claude-3-5-sonnet-20240620"", ""role"": ""assistant"", ""stop_reason"": ""end_turn"", ""stop_sequence"": null, ""type"": ""message"", ""usage"": { ""input_tokens"": 10, ""output_tokens"": 25 } }" ); var request = new MessageRequest( model: AnthropicModels.Claude35Sonnet, messages: [ new(MessageRole.User, [new TextContent("Hello!")]), ] ); var result = await Client.CreateMessageAsync(request); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); var message = result.Value; message.Id.Should().Be("msg_01D7FLrfh4GYq7yT1ULFeyMV"); message.Model.Should().Be("claude-3-5-sonnet-20240620"); message.Role.Should().Be("assistant"); message.StopReason.Should().Be("end_turn"); message.StopSequence.Should().BeNull(); message.Type.Should().Be("message"); message.Usage.InputTokens.Should().Be(10); message.Usage.OutputTokens.Should().Be(25); message.Content.Should().HaveCount(1); message.ToolCall.Should().BeNull(); var toolUseContent = message.Content[0]; toolUseContent.Should().BeOfType(); toolUseContent.As().Id.Should().Be("toolu_01D7FLrfh4GYq7yT1ULFeyMV"); toolUseContent.As().Name.Should().Be("get_stock_price"); toolUseContent.As().Type.Should().Be("tool_use"); var input = toolUseContent.As().Input; var ticker = input.GetValueOrDefault("ticker"); ticker!.ToString().Should().Be("^GSPC"); } [Theory] [ClassData(typeof(EventTestData))] public async Task CreateMessageAsync_WhenCalledAndMessageIsStreamed_ItShouldHandleAllEventTypes(string eventString, AnthropicEvent anthropicEvent) { _mockHttpMessageHandler .WhenCreateStreamMessageRequest() .Respond( HttpStatusCode.OK, "text/event-stream", new MemoryStream(Encoding.UTF8.GetBytes(eventString)) ); var request = new StreamMessageRequest( model: AnthropicModels.Claude35Sonnet, messages: [ new(MessageRole.User, [new TextContent("Hello!")]), ] ); var result = Client.CreateMessageAsync(request); var e = await result.FirstOrDefaultAsync(); e.Should().BeEquivalentTo(anthropicEvent); } [Fact] public async Task CreateMessageAsync_WhenCalledAndMessageIsStreamed_ItShouldReturnAllExpectedEvents() { var eventStream = EventTestData.GetEventStream(); var events = EventTestData.GetAllEvents(); _mockHttpMessageHandler .WhenCreateStreamMessageRequest() .Respond( HttpStatusCode.OK, "text/event-stream", eventStream ); var request = new StreamMessageRequest( model: AnthropicModels.Claude35Sonnet, messages: [ new(MessageRole.User, [new TextContent("Hello!")]), ] ); var result = Client.CreateMessageAsync(request); var actualEvents = await result.ToListAsync(); actualEvents.Should().BeEquivalentTo(events); } [Fact] public async Task CreateMessageAsync_WhenCalledMessageIsStreamedAndToolProvide_ItShouldReturnToolCall() { var eventStream = EventTestData.GetEventStream(); _mockHttpMessageHandler .WhenCreateStreamMessageRequest() .Respond( HttpStatusCode.OK, "text/event-stream", eventStream ); var getWeather = (string location, string unit) => $"The weather in {location} is 72°{unit}"; var request = new StreamMessageRequest( model: AnthropicModels.Claude35Sonnet, messages: [ new(MessageRole.User, [new TextContent("Hello!")]), ], tools: [ Tool.CreateFromFunction("get_weather", "Gets the weather for a location", getWeather) ] ); var result = Client.CreateMessageAsync(request); var msgCompleteEvent = await result .Where(e => e.Type is EventType.MessageComplete) .FirstAsync(); msgCompleteEvent.Data.Should().BeOfType(); var toolCall = msgCompleteEvent.Data.As().Message.ToolCall; var toolCallResult = await toolCall!.InvokeAsync(); toolCallResult.IsSuccess.Should().BeTrue(); toolCallResult.Value.Should().Be(getWeather("San Francisco, CA", "fahrenheit")); } [Fact] public async Task CreateMessageAsync_WhenCalledMessageIsStreamAndRequestFails_ItShouldReturnErrorEvent() { _mockHttpMessageHandler .WhenCreateStreamMessageRequest() .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 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 InvalidRequestError( "messages: roles must alternate between user and assistant, but found multiple user roles in a row" ) )); } [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() { _mockHttpMessageHandler .WhenCreateMessageRequest() .Respond( HttpStatusCode.OK, "application/json", @"{ ""content"": [ { ""text"": ""It is a PDF"", ""type"": ""text"" } ], ""id"": ""msg_013Zva2CMHLNnXjNJJKqJ2EF"", ""model"": ""claude-3-5-sonnet-20240620"", ""role"": ""assistant"", ""stop_reason"": ""end_turn"", ""stop_sequence"": null, ""type"": ""message"", ""usage"": { ""input_tokens"": 10, ""output_tokens"": 25 } }" ); var request = new MessageRequest( model: AnthropicModels.Claude35Sonnet, messages: [ new(MessageRole.User, [new TextContent("What is this?")]), new(MessageRole.User, [new DocumentContent("application/pdf", "data")]) ] ); var result = await Client.CreateMessageAsync(request); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); var message = result.Value; message.Id.Should().Be("msg_013Zva2CMHLNnXjNJJKqJ2EF"); message.Model.Should().Be("claude-3-5-sonnet-20240620"); message.Role.Should().Be("assistant"); message.StopReason.Should().Be("end_turn"); message.StopSequence.Should().BeNull(); message.Type.Should().Be("message"); message.Usage.InputTokens.Should().Be(10); message.Usage.OutputTokens.Should().Be(25); message.Content.Should().HaveCount(1); message.ToolCall.Should().BeNull(); var textContent = message.Content[0]; textContent.Should().BeOfType(); textContent.As().Text.Should().Be("It is a PDF"); textContent.As().Type.Should().Be("text"); } [Fact] public async Task CountMessageTokensAsync_WhenCalled_ItShouldReturnCountTokensResponse() { _mockHttpMessageHandler .WhenCountMessageTokensRequest() .Respond( HttpStatusCode.OK, "application/json", @"{ ""input_tokens"": 10 }" ); 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(10); } [Fact] public async Task CountMessageTokensAsync_WhenCalledAndErrorReturned_ItShouldHandleError() { _mockHttpMessageHandler .WhenCountMessageTokensRequest() .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 CountMessageTokensRequest( model: AnthropicModels.Claude35Sonnet, messages: [ new(MessageRole.User, [new TextContent("Hello!")]), new(MessageRole.User, [new TextContent("Hello!")]) ] ); var result = await Client.CountMessageTokensAsync(request); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task CountMessageTokensAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() { _mockHttpMessageHandler .WhenCountMessageTokensRequest() .Respond( HttpStatusCode.BadRequest, "application/json", @"null" ); var request = new CountMessageTokensRequest( model: AnthropicModels.Claude35Sonnet, messages: [ new(MessageRole.User, [new TextContent("Hello!")]), new(MessageRole.User, [new TextContent("Hello!")]) ] ); var result = await Client.CountMessageTokensAsync(request); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); 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() { _mockHttpMessageHandler .WhenListModelsRequest() .Respond( HttpStatusCode.OK, "application/json", @"{ ""data"": [ { ""type"": ""model"", ""id"": ""claude-3-5-sonnet-20241022"", ""display_name"": ""Claude 3.5 Sonnet (New)"", ""created_at"": ""2024-10-22T00:00:00Z"" } ], ""has_more"": true, ""first_id"": ""first_id"", ""last_id"": ""last_id"" }" ); var result = await Client.ListModelsAsync(); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType>(); result.Value.HasMore.Should().BeTrue(); result.Value.FirstId.Should().Be("first_id"); result.Value.LastId.Should().Be("last_id"); result.Value.Data.Should().BeEquivalentTo(new AnthropicModel[] { new() { Type = "model", Id = "claude-3-5-sonnet-20241022", DisplayName = "Claude 3.5 Sonnet (New)", CreatedAt = DateTimeOffset.Parse("2024-10-22T00:00:00Z") } }); } [Fact] public async Task ListModelsAsync_WhenCalledWithPagingRequestUsingCustomValues_ItShouldReturnListOfModels() { var pagingRequest = new PagingRequest(afterId: "next_id", limit: 10); _mockHttpMessageHandler .WhenListModelsRequest() .WithQueryString(new Dictionary { { "after_id", pagingRequest.AfterId }, { "limit", pagingRequest.Limit.ToString() }, }) .Respond( HttpStatusCode.OK, "application/json", @"{ ""data"": [ { ""type"": ""model"", ""id"": ""claude-3-5-sonnet-20241022"", ""display_name"": ""Claude 3.5 Sonnet (New)"", ""created_at"": ""2024-10-22T00:00:00Z"" } ], ""has_more"": true, ""first_id"": ""first_id"", ""last_id"": ""last_id"" }" ); var result = await Client.ListModelsAsync(pagingRequest); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType>(); result.Value.HasMore.Should().BeTrue(); result.Value.FirstId.Should().Be("first_id"); result.Value.LastId.Should().Be("last_id"); result.Value.Data.Should().BeEquivalentTo(new AnthropicModel[] { new() { Type = "model", Id = "claude-3-5-sonnet-20241022", DisplayName = "Claude 3.5 Sonnet (New)", CreatedAt = DateTimeOffset.Parse("2024-10-22T00:00:00Z") } }); } [Fact] public async Task ListModelsAsync_WhenCalledAndNoModelsReturned_ItShouldReturnEmptyList() { _mockHttpMessageHandler .WhenListModelsRequest() .Respond( HttpStatusCode.OK, "application/json", @"{ ""data"": [], ""has_more"": false, ""first_id"": null, ""last_id"": null }" ); var result = await Client.ListModelsAsync(); 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 ListModelsAsync_WhenCalledAndErrorReturned_ItShouldHandleError() { _mockHttpMessageHandler .WhenListModelsRequest() .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.ListModelsAsync(); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task ListModelsAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() { _mockHttpMessageHandler .WhenListModelsRequest() .Respond( HttpStatusCode.BadRequest, "application/json", @"null" ); var result = await Client.ListModelsAsync(); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); 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() { _mockHttpMessageHandler .WhenListModelsRequest() .WithExactQueryString(new Dictionary() { { "limit", "20" }, }) .Respond( HttpStatusCode.OK, "application/json", @"{ ""data"": [ { ""type"": ""model"", ""id"": ""claude-3-5-sonnet-20241022"", ""display_name"": ""Claude 3.5 Sonnet (New)"", ""created_at"": ""2024-10-22T00:00:00Z"" } ], ""has_more"": true, ""first_id"": ""1"", ""last_id"": ""1"" }" ); _mockHttpMessageHandler .WhenListModelsRequest() .WithExactQueryString(new Dictionary() { { "after_id", "1" }, { "limit", "20" }, }) .Respond( HttpStatusCode.OK, "application/json", @"{ ""data"": [ { ""type"": ""model"", ""id"": ""claude-3-5-sonnet-20241023"", ""display_name"": ""Claude 3.5 Sonnet (New)"", ""created_at"": ""2024-10-23T00:00:00Z"" } ], ""has_more"": false, ""first_id"": ""2"", ""last_id"": ""2"" }" ); var pageResponses = Client.ListAllModelsAsync(); var collectedPages = new List>(); await foreach (var response in pageResponses) { response.IsSuccess.Should().BeTrue(); response.Value.Should().BeOfType>(); collectedPages.Add(response.Value); } collectedPages.Should().HaveCount(2); collectedPages.Should().BeEquivalentTo(new Page[] { new() { HasMore = true, FirstId = "1", LastId = "1", Data = [ new() { Type = "model", Id = "claude-3-5-sonnet-20241022", DisplayName = "Claude 3.5 Sonnet (New)", CreatedAt = DateTimeOffset.Parse("2024-10-22T00:00:00Z") } ] }, new() { HasMore = false, FirstId = "2", LastId = "2", Data = [ new() { Type = "model", Id = "claude-3-5-sonnet-20241023", DisplayName = "Claude 3.5 Sonnet (New)", CreatedAt = DateTimeOffset.Parse("2024-10-23T00:00:00Z") } ] } }); } [Fact] public async Task ListAllModelsAsync_WhenCalledAndErrorReturned_ItShouldHandleError() { _mockHttpMessageHandler .WhenListModelsRequest() .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 responses = Client.ListAllModelsAsync(); var count = 0; await foreach (var page in responses) { count++; page.IsSuccess.Should().BeFalse(); page.Error.Should().BeOfType(); page.Error.Error.Should().BeOfType(); } count.Should().Be(1); } [Fact] public async Task ListAllModelsAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() { _mockHttpMessageHandler .WhenListModelsRequest() .Respond( HttpStatusCode.BadRequest, "application/json", @"null" ); var responses = Client.ListAllModelsAsync(); var count = 0; await foreach (var page in responses) { count++; page.IsSuccess.Should().BeFalse(); page.Error.Should().BeOfType(); page.Error.Error.Should().BeOfType(); } count.Should().Be(1); } [Fact] public async Task ListAllModelsAsync_WhenFirstPageSucceedsAndSecondPageFails_ItShouldReturnFirstPageAndError() { _mockHttpMessageHandler .WhenListModelsRequest() .WithExactQueryString(new Dictionary { { "limit", "20" }, }) .Respond( HttpStatusCode.OK, "application/json", @"{ ""data"": [ { ""type"": ""model"", ""id"": ""claude-3-5-sonnet-20241022"", ""display_name"": ""Claude 3.5 Sonnet (New)"", ""created_at"": ""2024-10-22T00:00:00Z"" } ], ""has_more"": true, ""first_id"": ""1"", ""last_id"": ""1"" }" ); _mockHttpMessageHandler .WhenListModelsRequest() .WithExactQueryString(new Dictionary { { "after_id", "1" }, { "limit", "20" }, }) .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 responses = Client.ListAllModelsAsync(); var count = 0; await foreach (var page in responses) { count++; if (count == 1) { page.IsSuccess.Should().BeTrue(); page.Value.Should().BeOfType>(); } else { page.IsSuccess.Should().BeFalse(); page.Error.Should().BeOfType(); page.Error.Error.Should().BeOfType(); } } 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() { var modelId = "claude-3-5-sonnet-20241022"; _mockHttpMessageHandler .WhenGetModelRequest(modelId) .Respond( HttpStatusCode.OK, "application/json", @"{ ""type"": ""model"", ""id"": ""claude-3-5-sonnet-20241022"", ""display_name"": ""Claude 3.5 Sonnet (New)"", ""created_at"": ""2024-10-22T00:00:00Z"" }" ); var result = await Client.GetModelAsync(modelId); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); result.Value.Type.Should().Be("model"); result.Value.Id.Should().Be("claude-3-5-sonnet-20241022"); result.Value.DisplayName.Should().Be("Claude 3.5 Sonnet (New)"); result.Value.CreatedAt.Should().Be(DateTimeOffset.Parse("2024-10-22T00:00:00Z")); } [Fact] public async Task GetModelAsync_WhenCalledAndErrorReturned_ItShouldHandleError() { var modelId = "claude-3-5-sonnet-20241022"; _mockHttpMessageHandler .WhenGetModelRequest(modelId) .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.GetModelAsync(modelId); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task GetModelAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() { var modelId = "claude-3-5-sonnet-20241022"; _mockHttpMessageHandler .WhenGetModelRequest(modelId) .Respond( HttpStatusCode.BadRequest, "application/json", @"null" ); var result = await Client.GetModelAsync(modelId); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task GetModelAsync_WhenCalledAndCanNotDeserializeModel_ItShouldReturnEmptyModel() { var modelId = "claude-3-5-sonnet-20241022"; _mockHttpMessageHandler .WhenGetModelRequest(modelId) .Respond( HttpStatusCode.OK, "application/json", @"null" ); var result = await Client.GetModelAsync(modelId); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); } [Fact] public async Task CreateMessageBatchAsync_WhenCalled_ItShouldReturnBatch() { _mockHttpMessageHandler .WhenCreateMessageBatchRequest() .Respond( HttpStatusCode.OK, "application/json", @"{ ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", ""type"": ""message_batch"", ""processing_status"": ""in_progress"", ""request_counts"": { ""processing"": 100, ""succeeded"": 50, ""errored"": 30, ""canceled"": 10, ""expired"": 10 }, ""ended_at"": ""2024-08-20T18:37:24.100435Z"", ""created_at"": ""2024-08-20T18:37:24.100435Z"", ""expires_at"": ""2024-08-20T18:37:24.100435Z"", ""archived_at"": ""2024-08-20T18:37:24.100435Z"", ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" }" ); var request = new MessageBatchRequest([new("custom_id", new())]); var result = await Client.CreateMessageBatchAsync(request); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); result.Value.Id.Should().Be("msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"); result.Value.Type.Should().Be("message_batch"); result.Value.ProcessingStatus.Should().Be("in_progress"); result.Value.RequestCounts.Should().BeEquivalentTo(new MessageBatchRequestCounts { Processing = 100, Succeeded = 50, Errored = 30, Canceled = 10, Expired = 10 }); result.Value.EndedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); result.Value.CreatedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); result.Value.ExpiresAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); result.Value.ArchivedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); result.Value.CancelInitiatedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); result.Value.ResultsUrl.Should().Be("https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"); } [Fact] public async Task CreateMessageBatchAsync_WhenCalledAndErrorReturned_ItShouldHandleError() { _mockHttpMessageHandler .WhenCreateMessageBatchRequest() .Respond( HttpStatusCode.BadRequest, "application/json", @"{ ""type"": ""error"", ""error"": { ""type"": ""invalid_request_error"", ""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row"" } }" ); var request = new MessageBatchRequest([new("custom_id", new())]); var result = await Client.CreateMessageBatchAsync(request); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task CreateMessageBatchAsync_WhenCalledRequestFailsAndErrorCanNotBeDeserialized_ItShouldReturnUnknownError() { _mockHttpMessageHandler .WhenCreateMessageBatchRequest() .Respond( HttpStatusCode.BadRequest, "application/json", @"null" ); var request = new MessageBatchRequest([new("custom_id", new())]); var result = await Client.CreateMessageBatchAsync(request); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task CreateMessageBatchAsync_WhenCalledRequestSucceedsAndCanNotDeserializeResponse_ItShouldReturnEmptyResponse() { _mockHttpMessageHandler .WhenCreateMessageBatchRequest() .Respond( HttpStatusCode.OK, "application/json", @"null" ); var request = new MessageBatchRequest([new("custom_id", new())]); var result = await Client.CreateMessageBatchAsync(request); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); result.Value.Should().BeEquivalentTo(new MessageBatchResponse()); } [Fact] public async Task GetMessageBatchAsync_WhenCalled_ItShouldReturnBatch() { var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; _mockHttpMessageHandler .WhenGetMessageBatchRequest(batchId) .Respond( HttpStatusCode.OK, "application/json", @"{ ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", ""type"": ""message_batch"", ""processing_status"": ""in_progress"", ""request_counts"": { ""processing"": 100, ""succeeded"": 50, ""errored"": 30, ""canceled"": 10, ""expired"": 10 }, ""ended_at"": ""2024-08-20T18:37:24.100435Z"", ""created_at"": ""2024-08-20T18:37:24.100435Z"", ""expires_at"": ""2024-08-20T18:37:24.100435Z"", ""archived_at"": ""2024-08-20T18:37:24.100435Z"", ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" }" ); var result = await Client.GetMessageBatchAsync(batchId); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeEquivalentTo(new MessageBatchResponse() { Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", Type = "message_batch", ProcessingStatus = "in_progress", RequestCounts = new MessageBatchRequestCounts { Processing = 100, Succeeded = 50, Errored = 30, Canceled = 10, Expired = 10 }, EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" }); } [Fact] public async Task GetMessageBatchAsync_WhenCalledAndErrorReturned_ItShouldHandleError() { var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; _mockHttpMessageHandler .WhenGetMessageBatchRequest(batchId) .Respond( HttpStatusCode.BadRequest, "application/json", @"{ ""type"": ""error"", ""error"": { ""type"": ""invalid_request_error"", ""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row"" } }" ); var result = await Client.GetMessageBatchAsync(batchId); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task GetMessageBatchAsync_WhenCalledRequestFailsAndErrorCanNotBeDeserialized_ItShouldReturnUnknownError() { var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; _mockHttpMessageHandler .WhenGetMessageBatchRequest(batchId) .Respond( HttpStatusCode.BadRequest, "application/json", @"null" ); var result = await Client.GetMessageBatchAsync(batchId); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task GetMessageBatchAsync_WhenCalledAndCanNotDeserializeResponse_ItShouldReturnEmptyResponse() { var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; _mockHttpMessageHandler .WhenGetMessageBatchRequest(batchId) .Respond( HttpStatusCode.OK, "application/json", @"null" ); var result = await Client.GetMessageBatchAsync(batchId); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); } [Fact] public async Task GetMessageBatchResultsAsync_WhenCalledAndSuccessful_ItShouldReturnAllBatchResults() { var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; var batchResultsPath = TestFileHelper.GetTestFilePath("batch_results.jsonl"); var batchResultsText = await File.ReadAllTextAsync(batchResultsPath); var batchResults = batchResultsText.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); var expectedResults = batchResults.Select(Deserialize); _mockHttpMessageHandler .WhenGetMessageBatchResultsRequest(batchId) .Respond( HttpStatusCode.OK, "application/x-jsonl", batchResultsText ); var result = await Client.GetMessageBatchResultsAsync(batchId); result.IsSuccess.Should().BeTrue(); var actualResults = await result.Value.ToListAsync(); actualResults.Should().BeEquivalentTo(expectedResults); } [Fact] public async Task GetMessageBatchResultsAsync_WhenCalledSuccessfulAndResultCanNotBeDeserialized_ItShouldReturnEmptyResults() { var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; var expectedResults = new List() { new(), }; _mockHttpMessageHandler .WhenGetMessageBatchResultsRequest(batchId) .Respond( HttpStatusCode.OK, "application/x-jsonl", "null" ); var result = await Client.GetMessageBatchResultsAsync(batchId); result.IsSuccess.Should().BeTrue(); var actualResults = await result.Value.ToListAsync(); actualResults.Should().BeEquivalentTo(expectedResults); } [Fact] public async Task GetMessageBatchResultsAsync_WhenCalledAndRequestFails_ItShouldReturnError() { var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; _mockHttpMessageHandler .WhenGetMessageBatchResultsRequest(batchId) .Respond( HttpStatusCode.BadRequest, "application/json", @"{ ""type"": ""error"", ""error"": { ""type"": ""invalid_request_error"", ""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row"" } }" ); var result = await Client.GetMessageBatchResultsAsync(batchId); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task GetMessageBatchResultsAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() { var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; _mockHttpMessageHandler .WhenGetMessageBatchResultsRequest(batchId) .Respond( HttpStatusCode.BadRequest, "application/json", @"null" ); var result = await Client.GetMessageBatchResultsAsync(batchId); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task ListMessageBatchesAsync_WhenCalledAndSuccessful_ItShouldReturnPageOfBatches() { _mockHttpMessageHandler .WhenListMessageBatchesRequest() .Respond( HttpStatusCode.OK, "application/json", @"{ ""data"": [ { ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", ""type"": ""message_batch"", ""processing_status"": ""in_progress"", ""request_counts"": { ""processing"": 100, ""succeeded"": 50, ""errored"": 30, ""canceled"": 10, ""expired"": 10 }, ""ended_at"": ""2024-08-20T18:37:24.100435Z"", ""created_at"": ""2024-08-20T18:37:24.100435Z"", ""expires_at"": ""2024-08-20T18:37:24.100435Z"", ""archived_at"": ""2024-08-20T18:37:24.100435Z"", ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" } ], ""has_more"": true, ""first_id"": ""1"", ""last_id"": ""1"" }" ); var result = await Client.ListMessageBatchesAsync(); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType>(); result.Value.HasMore.Should().BeTrue(); result.Value.FirstId.Should().Be("1"); result.Value.LastId.Should().Be("1"); result.Value.Data.Should().BeEquivalentTo(new MessageBatchResponse[] { new() { Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", Type = "message_batch", ProcessingStatus = "in_progress", RequestCounts = new MessageBatchRequestCounts { Processing = 100, Succeeded = 50, Errored = 30, Canceled = 10, Expired = 10 }, EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" } }); } [Fact] public async Task ListMessageBatchesAsync_WhenCalledWithPagingRequestAndSuccessful_ItShouldReturnPageOfBatches() { var pagingRequest = new PagingRequest(afterId: "next_id", limit: 10); _mockHttpMessageHandler .WhenListMessageBatchesRequest() .WithQueryString(new Dictionary { { "after_id", pagingRequest.AfterId }, { "limit", pagingRequest.Limit.ToString() }, }) .Respond( HttpStatusCode.OK, "application/json", @"{ ""data"": [ { ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", ""type"": ""message_batch"", ""processing_status"": ""in_progress"", ""request_counts"": { ""processing"": 100, ""succeeded"": 50, ""errored"": 30, ""canceled"": 10, ""expired"": 10 }, ""ended_at"": ""2024-08-20T18:37:24.100435Z"", ""created_at"": ""2024-08-20T18:37:24.100435Z"", ""expires_at"": ""2024-08-20T18:37:24.100435Z"", ""archived_at"": ""2024-08-20T18:37:24.100435Z"", ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" } ], ""has_more"": true, ""first_id"": ""1"", ""last_id"": ""1"" }" ); var result = await Client.ListMessageBatchesAsync(pagingRequest); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType>(); result.Value.HasMore.Should().BeTrue(); result.Value.FirstId.Should().Be("1"); result.Value.LastId.Should().Be("1"); result.Value.Data.Should().BeEquivalentTo(new MessageBatchResponse[] { new() { Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", Type = "message_batch", ProcessingStatus = "in_progress", RequestCounts = new MessageBatchRequestCounts { Processing = 100, Succeeded = 50, Errored = 30, Canceled = 10, Expired = 10 }, EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" } }); } [Fact] public async Task ListMessageBatchesAsync_WhenCalledAndNoBatchesReturned_ItShouldReturnEmptyList() { _mockHttpMessageHandler .WhenListMessageBatchesRequest() .Respond( HttpStatusCode.OK, "application/json", @"{ ""data"": [], ""has_more"": false, ""first_id"": null, ""last_id"": null }" ); var result = await Client.ListMessageBatchesAsync(); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType>(); result.Value.HasMore.Should().BeFalse(); result.Value.FirstId.Should().BeNull(); result.Value.LastId.Should().BeNull(); result.Value.Data.Should().BeEmpty(); } [Fact] public async Task ListMessageBatchesAsync_WhenCalledAndErrorReturned_ItShouldHandleError() { _mockHttpMessageHandler .WhenListMessageBatchesRequest() .Respond( HttpStatusCode.BadRequest, "application/json", @"{ ""type"": ""error"", ""error"": { ""type"": ""invalid_request_error"", ""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row"" } }" ); var result = await Client.ListMessageBatchesAsync(); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task ListMessageBatchesAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() { _mockHttpMessageHandler .WhenListMessageBatchesRequest() .Respond( HttpStatusCode.BadRequest, "application/json", @"null" ); var result = await Client.ListMessageBatchesAsync(); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task ListMessageBatchesAsync_WhenCalledRequestSucceedsAndCanNotDeserializeResponse_ItShouldReturnEmptyPage() { _mockHttpMessageHandler .WhenListMessageBatchesRequest() .Respond( HttpStatusCode.OK, "application/json", @"null" ); var result = await Client.ListMessageBatchesAsync(); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType>(); result.Value.HasMore.Should().BeFalse(); result.Value.FirstId.Should().BeEmpty(); result.Value.LastId.Should().BeEmpty(); result.Value.Data.Should().BeEmpty(); } [Fact] public async Task ListAllMessageBatchesAsync_WhenCalled_ItShouldReturnAllBatches() { _mockHttpMessageHandler .WhenListMessageBatchesRequest() .WithExactQueryString(new Dictionary() { { "limit", "20" }, }) .Respond( HttpStatusCode.OK, "application/json", @"{ ""data"": [ { ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", ""type"": ""message_batch"", ""processing_status"": ""in_progress"", ""request_counts"": { ""processing"": 100, ""succeeded"": 50, ""errored"": 30, ""canceled"": 10, ""expired"": 10 }, ""ended_at"": ""2024-08-20T18:37:24.100435Z"", ""created_at"": ""2024-08-20T18:37:24.100435Z"", ""expires_at"": ""2024-08-20T18:37:24.100435Z"", ""archived_at"": ""2024-08-20T18:37:24.100435Z"", ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" } ], ""has_more"": true, ""first_id"": ""1"", ""last_id"": ""1"" }" ); _mockHttpMessageHandler .WhenListMessageBatchesRequest() .WithExactQueryString(new Dictionary() { { "after_id", "1" }, { "limit", "20" }, }) .Respond( HttpStatusCode.OK, "application/json", @"{ ""data"": [ { ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", ""type"": ""message_batch"", ""processing_status"": ""in_progress"", ""request_counts"": { ""processing"": 100, ""succeeded"": 50, ""errored"": 30, ""canceled"": 10, ""expired"": 10 }, ""ended_at"": ""2024-08-20T18:37:24.100435Z"", ""created_at"": ""2024-08-20T18:37:24.100435Z"", ""expires_at"": ""2024-08-20T18:37:24.100435Z"", ""archived_at"": ""2024-08-20T18:37:24.100435Z"", ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" } ], ""has_more"": false, ""first_id"": ""2"", ""last_id"": ""2"" }" ); var pageResponses = Client.ListAllMessageBatchesAsync(); var collectedPages = new List>(); await foreach (var response in pageResponses) { response.IsSuccess.Should().BeTrue(); response.Value.Should().BeOfType>(); collectedPages.Add(response.Value); } var expectedMessageBatchResponse = new MessageBatchResponse() { Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", Type = "message_batch", ProcessingStatus = "in_progress", RequestCounts = new MessageBatchRequestCounts { Processing = 100, Succeeded = 50, Errored = 30, Canceled = 10, Expired = 10 }, EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" }; collectedPages.Should().HaveCount(2); collectedPages.Should().BeEquivalentTo(new List>() { new() { Data = [expectedMessageBatchResponse], FirstId = "1", LastId = "1", HasMore = true }, new() { Data = [expectedMessageBatchResponse], FirstId = "2", LastId = "2", HasMore = false } }); } [Fact] public async Task CancelMessageBatchAsync_WhenCalled_ItShouldReturnBatch() { var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; _mockHttpMessageHandler .WhenCancelMessageBatchRequest(batchId) .Respond( HttpStatusCode.OK, "application/json", @"{ ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", ""type"": ""message_batch"", ""processing_status"": ""in_progress"", ""request_counts"": { ""processing"": 100, ""succeeded"": 50, ""errored"": 30, ""canceled"": 10, ""expired"": 10 }, ""ended_at"": ""2024-08-20T18:37:24.100435Z"", ""created_at"": ""2024-08-20T18:37:24.100435Z"", ""expires_at"": ""2024-08-20T18:37:24.100435Z"", ""archived_at"": ""2024-08-20T18:37:24.100435Z"", ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"", ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"" }" ); var result = await Client.CancelMessageBatchAsync(batchId); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeEquivalentTo(new MessageBatchResponse() { Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", Type = "message_batch", ProcessingStatus = "in_progress", RequestCounts = new MessageBatchRequestCounts { Processing = 100, Succeeded = 50, Errored = 30, Canceled = 10, Expired = 10 }, EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"), ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results" }); } [Fact] public async Task CancelMessageBatchAsync_WhenCalledAndFails_ItShouldReturnError() { var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; _mockHttpMessageHandler .WhenCancelMessageBatchRequest(batchId) .Respond( HttpStatusCode.BadRequest, "application/json", @"{ ""type"": ""error"", ""error"": { ""type"": ""invalid_request_error"", ""message"": ""batch: batch not found"" } }" ); var result = await Client.CancelMessageBatchAsync(batchId); result.IsSuccess.Should().BeFalse(); result.Error.Should().BeOfType(); result.Error.Error.Should().BeOfType(); } [Fact] public async Task DeleteMessageBatchAsync_WhenCalled_ItShouldReturnDeletionResponse() { var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; _mockHttpMessageHandler .WhenDeleteMessageBatchRequest(batchId) .Respond( HttpStatusCode.OK, "application/json", @"{ ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"", ""type"": ""message_batch_deleted"" }" ); var result = await Client.DeleteMessageBatchAsync(batchId); result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); result.Value.Id.Should().Be(batchId); result.Value.Type.Should().Be("message_batch_deleted"); } }