From 8bbd4bb7c8c3f4e6fdd6d85d68d728794b85831f Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 5 Jan 2025 21:36:34 -0600 Subject: [PATCH] feat: implement GetModelAsync method --- src/AnthropicClient/AnthropicApiClient.cs | 25 +++++ .../EndToEnd/AnthropicApiClientTests.cs | 10 ++ .../Integration/AnthropicApiClientTests.cs | 99 ++++++++++++++++++- .../Integration/IntegrationTest.cs | 6 ++ 4 files changed, 137 insertions(+), 3 deletions(-) diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs index 836a6bd..3be31ee 100644 --- a/src/AnthropicClient/AnthropicApiClient.cs +++ b/src/AnthropicClient/AnthropicApiClient.cs @@ -49,6 +49,13 @@ public interface IAnthropicApiClient /// 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); } /// @@ -352,6 +359,24 @@ public class AnthropicApiClient : IAnthropicApiClient } while (hasMore); } + /// + public async Task> GetModelAsync(string modelId) + { + var endpoint = $"{ModelsEndpoint}/{modelId}"; + var response = await SendRequestAsync(endpoint); + var anthropicHeaders = new AnthropicHeaders(response.Headers); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode is false) + { + var error = Deserialize(responseContent) ?? new AnthropicError(); + return AnthropicResult.Failure(error, anthropicHeaders); + } + + var model = Deserialize(responseContent) ?? new AnthropicModel(); + return AnthropicResult.Success(model, anthropicHeaders); + } + private ToolCall? GetToolCall(MessageResponse response, List tools) { var toolUse = response.Content.OfType().FirstOrDefault(); diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs index e64893a..47ccadd 100644 --- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs @@ -331,4 +331,14 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf responses.Should().HaveCountGreaterThan(0); } + + [Fact] + public async Task GetModelAsync_WhenCalled_ItShouldReturnResponse() + { + var result = await _client.GetModelAsync(AnthropicModels.Claude3Haiku); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType(); + result.Value.Id.Should().Be(AnthropicModels.Claude3Haiku); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs index 795b132..2dcb1be 100644 --- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs @@ -479,7 +479,7 @@ public class AnthropicApiClientTests : IntegrationTest } [Fact] - public async Task CountMessageTokensAsync_WhenCalledRequestFailsAndCanNotSerializeError_ItShouldReturnUnknownError() + public async Task CountMessageTokensAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() { _mockHttpMessageHandler .WhenCountMessageTokensRequest() @@ -647,7 +647,7 @@ public class AnthropicApiClientTests : IntegrationTest } [Fact] - public async Task ListModelsAsync_WhenCalledRequestFailsAndCanNotSerializeError_ItShouldReturnUnknownError() + public async Task ListModelsAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() { _mockHttpMessageHandler .WhenListModelsRequest() @@ -794,7 +794,7 @@ public class AnthropicApiClientTests : IntegrationTest } [Fact] - public async Task ListAllModelsAsync_WhenCalledRequestFailsAndCanNotSerializeError_ItShouldReturnUnknownError() + public async Task ListAllModelsAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError() { _mockHttpMessageHandler .WhenListModelsRequest() @@ -886,4 +886,97 @@ public class AnthropicApiClientTests : IntegrationTest count.Should().Be(2); } + + [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", + @"{}" + ); + + 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", + @"{}" + ); + + var result = await Client.GetModelAsync(modelId); + + result.IsSuccess.Should().BeFalse(); + result.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 1d5b77a..6b6376d 100644 --- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs +++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs @@ -58,4 +58,10 @@ public static class MockHttpMessageHandlerExtensions return mockHttpMessageHandler .SetupBaseRequest(HttpMethod.Get, ModelsEndpoint); } + + public static MockedRequest WhenGetModelRequest(this MockHttpMessageHandler mockHttpMessageHandler, string modelId) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Get, $"{ModelsEndpoint}/{modelId}"); + } } \ No newline at end of file