Merge pull request #30 from StevanFreeborn/stevanfreeborn/feat/add-support-for-models-API
feat: add support for models API
This commit is contained in:
Vendored
+1
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
"dotnet.defaultSolution": "AnthropicClient.sln",
|
"dotnet.defaultSolution": "AnthropicClient.sln",
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"Browsable",
|
"Browsable",
|
||||||
|
|||||||
@@ -134,6 +134,73 @@ if (response.IsFailure)
|
|||||||
Console.WriteLine("Token Count: {0}", response.Value.InputTokens);
|
Console.WriteLine("Token Count: {0}", response.Value.InputTokens);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### List Models
|
||||||
|
|
||||||
|
The `AnthropicApiClient` exposes a method named `ListModelsAsync` that can be used to list the available models. The method takes an optional `PagingRequest` instance as a parameter.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using AnthropicClient;
|
||||||
|
|
||||||
|
var response = await client.ListModelsAsync();
|
||||||
|
|
||||||
|
if (response.IsFailure)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to list models");
|
||||||
|
Console.WriteLine("Error Type: {0}", response.Error.Error.Type);
|
||||||
|
Console.WriteLine("Error Message: {0}", response.Error.Error.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var model in response.Value.Data)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Model Id: {0}", model.Id);
|
||||||
|
Console.WriteLine("Model Name: {0}", model.DisplayName);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Using the `PagingRequest` instance allows you to specify the number of models to return and the page of models to return.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using AnthropicClient;
|
||||||
|
using AnthropicClient.Models;
|
||||||
|
|
||||||
|
var response = await client.ListModelsAsync(new PagingRequest(afterId: "claude-3-5-sonnet-20241022", limit: 2));
|
||||||
|
|
||||||
|
if (response.IsFailure)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to list models");
|
||||||
|
Console.WriteLine("Error Type: {0}", response.Error.Error.Type);
|
||||||
|
Console.WriteLine("Error Message: {0}", response.Error.Error.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var model in response.Value.Data)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Model Id: {0}", model.Id);
|
||||||
|
Console.WriteLine("Model Name: {0}", model.DisplayName);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Model
|
||||||
|
|
||||||
|
The `AnthropicApiClient` exposes a method named `GetModelAsync` that can be used to get a model by its id.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using AnthropicClient;
|
||||||
|
|
||||||
|
var response = await client.GetModelAsync("claude-3-5-sonnet-20241022");
|
||||||
|
|
||||||
|
if (response.IsFailure)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to get model");
|
||||||
|
Console.WriteLine("Error Type: {0}", response.Error.Error.Type);
|
||||||
|
Console.WriteLine("Error Message: {0}", response.Error.Error.Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Model Id: {0}", response.Value.Id);
|
||||||
|
```
|
||||||
|
|
||||||
### Create a message
|
### Create a message
|
||||||
|
|
||||||
The `AnthropicApiClient` exposes a method named `CreateMessageAsync` that can be used to create a message. The method requires a `MessageRequest` or a `StreamMessageRequest` instance as a parameter. The `MessageRequest` class is used to create a message whose response is not streamed and the `StreamMessageRequest` class is used to create a message whose response is streamed. The `MessageRequest` instance's properties can be set to configure how the message is created.
|
The `AnthropicApiClient` exposes a method named `CreateMessageAsync` that can be used to create a message. The method requires a `MessageRequest` or a `StreamMessageRequest` instance as a parameter. The `MessageRequest` class is used to create a message whose response is not streamed and the `StreamMessageRequest` class is used to create a message whose response is streamed. The `MessageRequest` instance's properties can be set to configure how the message is created.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AnthropicClient.Json;
|
using AnthropicClient.Json;
|
||||||
using AnthropicClient.Models;
|
using AnthropicClient.Models;
|
||||||
@@ -33,6 +34,28 @@ public interface IAnthropicApiClient
|
|||||||
/// <param name="request">The count message tokens request.</param>
|
/// <param name="request">The count message tokens request.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="TokenCountResponse"/>.</returns>
|
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="TokenCountResponse"/>.</returns>
|
||||||
Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request);
|
Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lists the models asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The paging request to use for listing the models.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="AnthropicModel"/>.</returns>
|
||||||
|
Task<AnthropicResult<Page<AnthropicModel>>> ListModelsAsync(PagingRequest? request = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lists the models asynchronously
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="limit">The maximum number of models to return in each page.</param>
|
||||||
|
/// <returns>An asynchronous enumerable that yields the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="AnthropicModel"/>.</returns>
|
||||||
|
///
|
||||||
|
IAsyncEnumerable<AnthropicResult<Page<AnthropicModel>>> ListAllModelsAsync(int limit = 20);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a model by its ID asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modelId">The ID of the model to get.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicModel"/>.</returns>
|
||||||
|
Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="IAnthropicApiClient"/>
|
/// <inheritdoc cref="IAnthropicApiClient"/>
|
||||||
@@ -42,6 +65,7 @@ public class AnthropicApiClient : IAnthropicApiClient
|
|||||||
private const string ApiKeyHeader = "x-api-key";
|
private const string ApiKeyHeader = "x-api-key";
|
||||||
private const string MessagesEndpoint = "messages";
|
private const string MessagesEndpoint = "messages";
|
||||||
private const string CountTokensEndpoint = "messages/count_tokens";
|
private const string CountTokensEndpoint = "messages/count_tokens";
|
||||||
|
private const string ModelsEndpoint = "models";
|
||||||
private const string JsonContentType = "application/json";
|
private const string JsonContentType = "application/json";
|
||||||
private const string EventPrefix = "event:";
|
private const string EventPrefix = "event:";
|
||||||
private const string DataPrefix = "data:";
|
private const string DataPrefix = "data:";
|
||||||
@@ -76,7 +100,7 @@ public class AnthropicApiClient : IAnthropicApiClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc/>
|
||||||
public async Task<AnthropicResult<MessageResponse>> CreateMessageAsync(MessageRequest request)
|
public async Task<AnthropicResult<MessageResponse>> CreateMessageAsync(MessageRequest request)
|
||||||
{
|
{
|
||||||
var response = await SendRequestAsync(MessagesEndpoint, request);
|
var response = await SendRequestAsync(MessagesEndpoint, request);
|
||||||
@@ -99,7 +123,7 @@ public class AnthropicApiClient : IAnthropicApiClient
|
|||||||
return AnthropicResult<MessageResponse>.Success(msgResponse, anthropicHeaders);
|
return AnthropicResult<MessageResponse>.Success(msgResponse, anthropicHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc/>
|
||||||
public async IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRequest request)
|
public async IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRequest request)
|
||||||
{
|
{
|
||||||
var response = await SendRequestAsync(MessagesEndpoint, request);
|
var response = await SendRequestAsync(MessagesEndpoint, request);
|
||||||
@@ -263,7 +287,7 @@ public class AnthropicApiClient : IAnthropicApiClient
|
|||||||
} while (true);
|
} while (true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc/>
|
||||||
public async Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request)
|
public async Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request)
|
||||||
{
|
{
|
||||||
var response = await SendRequestAsync(CountTokensEndpoint, request);
|
var response = await SendRequestAsync(CountTokensEndpoint, request);
|
||||||
@@ -277,10 +301,82 @@ public class AnthropicApiClient : IAnthropicApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
var msgResponse = Deserialize<TokenCountResponse>(responseContent) ?? new TokenCountResponse();
|
var msgResponse = Deserialize<TokenCountResponse>(responseContent) ?? new TokenCountResponse();
|
||||||
|
|
||||||
return AnthropicResult<TokenCountResponse>.Success(msgResponse, anthropicHeaders);
|
return AnthropicResult<TokenCountResponse>.Success(msgResponse, anthropicHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<AnthropicResult<Page<AnthropicModel>>> ListModelsAsync(PagingRequest? request = null)
|
||||||
|
{
|
||||||
|
var pagingRequest = request ?? new PagingRequest();
|
||||||
|
var endpoint = $"{ModelsEndpoint}?{pagingRequest.ToQueryParameters()}";
|
||||||
|
var response = await SendRequestAsync(endpoint);
|
||||||
|
var anthropicHeaders = new AnthropicHeaders(response.Headers);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode is false)
|
||||||
|
{
|
||||||
|
var error = Deserialize<AnthropicError>(responseContent) ?? new AnthropicError();
|
||||||
|
return AnthropicResult<Page<AnthropicModel>>.Failure(error, anthropicHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
var page = Deserialize<Page<AnthropicModel>>(responseContent) ?? new Page<AnthropicModel>();
|
||||||
|
return AnthropicResult<Page<AnthropicModel>>.Success(page, anthropicHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async IAsyncEnumerable<AnthropicResult<Page<AnthropicModel>>> ListAllModelsAsync(int limit = 20)
|
||||||
|
{
|
||||||
|
var pagingRequest = new PagingRequest(limit: limit);
|
||||||
|
string Endpoint() => $"{ModelsEndpoint}?{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<AnthropicError>(responseContent) ?? new AnthropicError();
|
||||||
|
yield return AnthropicResult<Page<AnthropicModel>>.Failure(error, anthropicHeaders);
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var page = Deserialize<Page<AnthropicModel>>(responseContent) ?? new Page<AnthropicModel>();
|
||||||
|
|
||||||
|
if (page.HasMore && page.LastId is not null)
|
||||||
|
{
|
||||||
|
hasMore = true;
|
||||||
|
pagingRequest = new PagingRequest(limit: limit, afterId: page.LastId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return AnthropicResult<Page<AnthropicModel>>.Success(page, anthropicHeaders);
|
||||||
|
} while (hasMore);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId)
|
||||||
|
{
|
||||||
|
var endpoint = $"{ModelsEndpoint}/{modelId}";
|
||||||
|
var response = await SendRequestAsync(endpoint);
|
||||||
|
var anthropicHeaders = new AnthropicHeaders(response.Headers);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode is false)
|
||||||
|
{
|
||||||
|
var error = Deserialize<AnthropicError>(responseContent) ?? new AnthropicError();
|
||||||
|
return AnthropicResult<AnthropicModel>.Failure(error, anthropicHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = Deserialize<AnthropicModel>(responseContent) ?? new AnthropicModel();
|
||||||
|
return AnthropicResult<AnthropicModel>.Success(model, anthropicHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
private ToolCall? GetToolCall(MessageResponse response, List<Tool> tools)
|
private ToolCall? GetToolCall(MessageResponse response, List<Tool> tools)
|
||||||
{
|
{
|
||||||
var toolUse = response.Content.OfType<ToolUseContent>().FirstOrDefault();
|
var toolUse = response.Content.OfType<ToolUseContent>().FirstOrDefault();
|
||||||
@@ -300,6 +396,11 @@ public class AnthropicApiClient : IAnthropicApiClient
|
|||||||
return new ToolCall(tool, toolUse);
|
return new ToolCall(tool, toolUse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> SendRequestAsync(string endpoint)
|
||||||
|
{
|
||||||
|
return await _httpClient.GetAsync(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> SendRequestAsync<T>(string endpoint, T request)
|
private async Task<HttpResponseMessage> SendRequestAsync<T>(string endpoint, T request)
|
||||||
{
|
{
|
||||||
var requestJson = Serialize(request);
|
var requestJson = Serialize(request);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace AnthropicClient.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an Anthropic model.
|
||||||
|
/// </summary>
|
||||||
|
public class AnthropicModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The type of the model.
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The id of the model.
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The display name of the model.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("display_name")]
|
||||||
|
public string DisplayName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The created date of the model.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("created_at")]
|
||||||
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace AnthropicClient.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a page.
|
||||||
|
/// </summary>
|
||||||
|
public class Page
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The id of the first item in the page.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("first_id")]
|
||||||
|
public string? FirstId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The id of the last item in the page.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("last_id")]
|
||||||
|
public string? LastId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether there is more data to be retrieved.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("has_more")]
|
||||||
|
public bool HasMore { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a page with data.
|
||||||
|
/// </summary>
|
||||||
|
public class Page<T> : Page
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The data in the page.
|
||||||
|
/// </summary>
|
||||||
|
public T[] Data { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace AnthropicClient.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request to page through a collection of items.
|
||||||
|
/// </summary>
|
||||||
|
public class PagingRequest
|
||||||
|
{
|
||||||
|
private const int LimitMinimum = 1;
|
||||||
|
private const int LimitMaximum = 1000;
|
||||||
|
private const int DefaultLimit = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the item before which to start the page.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("before_id")]
|
||||||
|
public string BeforeId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the item after which to start the page.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("after_id")]
|
||||||
|
public string AfterId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum number of items to return in the page.
|
||||||
|
/// </summary>
|
||||||
|
public int Limit { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="PagingRequest"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beforeId">The ID of the item before which to start the page.</param>
|
||||||
|
/// <param name="afterId">The ID of the item after which to start the page.</param>
|
||||||
|
/// <param name="limit">The maximum number of items to return in the page.</param>
|
||||||
|
/// <exception cref="ArgumentException">Thrown when both <paramref name="beforeId"/> and <paramref name="afterId"/> are specified.</exception>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="limit"/> is less than 1 or greater than 1000.</exception>
|
||||||
|
/// <returns>A new instance of the <see cref="PagingRequest"/> class.</returns>
|
||||||
|
public PagingRequest(
|
||||||
|
string beforeId = "",
|
||||||
|
string afterId = "",
|
||||||
|
int limit = DefaultLimit
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (limit is < LimitMinimum or > LimitMaximum)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(limit), $"{nameof(limit)} must be between {LimitMinimum} and {LimitMaximum}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(beforeId) is false && string.IsNullOrEmpty(afterId) is false)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Only one of {nameof(beforeId)} or {nameof(afterId)} can be set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
BeforeId = beforeId;
|
||||||
|
AfterId = afterId;
|
||||||
|
Limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the <see cref="PagingRequest"/> to a query string.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The query string representation of the <see cref="PagingRequest"/>.</returns>
|
||||||
|
public string ToQueryParameters()
|
||||||
|
{
|
||||||
|
var parameters = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(BeforeId) is false)
|
||||||
|
{
|
||||||
|
parameters.Add($"before_id={BeforeId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(AfterId) is false)
|
||||||
|
{
|
||||||
|
parameters.Add($"after_id={AfterId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters.Add($"limit={Limit}");
|
||||||
|
|
||||||
|
return string.Join("&", parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -303,4 +303,42 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
|
|||||||
result.Value.Should().BeOfType<TokenCountResponse>();
|
result.Value.Should().BeOfType<TokenCountResponse>();
|
||||||
result.Value.InputTokens.Should().BeGreaterThan(0);
|
result.Value.InputTokens.Should().BeGreaterThan(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListModelsAsync_WhenCalled_ItShouldReturnResponse()
|
||||||
|
{
|
||||||
|
var result = await _client.ListModelsAsync();
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().BeOfType<Page<AnthropicModel>>();
|
||||||
|
result.Value.Data.Should().HaveCountGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListModelsAsync_WhenCalledWithPagination_ItShouldReturnResponse()
|
||||||
|
{
|
||||||
|
var result = await _client.ListModelsAsync(new PagingRequest(limit: 1));
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().BeOfType<Page<AnthropicModel>>();
|
||||||
|
result.Value.Data.Should().HaveCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListAllModelsAsync_WhenCalled_ItShouldReturnResponse()
|
||||||
|
{
|
||||||
|
var responses = await _client.ListAllModelsAsync(limit: 1).ToListAsync();
|
||||||
|
|
||||||
|
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<AnthropicModel>();
|
||||||
|
result.Value.Id.Should().Be(AnthropicModels.Claude3Haiku);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ public class AnthropicApiClientTests : IntegrationTest
|
|||||||
""input_tokens"": 10,
|
""input_tokens"": 10,
|
||||||
""output_tokens"": 25
|
""output_tokens"": 25
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
);
|
);
|
||||||
|
|
||||||
var request = new MessageRequest(
|
var request = new MessageRequest(
|
||||||
@@ -115,7 +115,7 @@ public class AnthropicApiClientTests : IntegrationTest
|
|||||||
""input_tokens"": 10,
|
""input_tokens"": 10,
|
||||||
""output_tokens"": 25
|
""output_tokens"": 25
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
);
|
);
|
||||||
|
|
||||||
var func = (string ticker) => ticker;
|
var func = (string ticker) => ticker;
|
||||||
@@ -191,7 +191,7 @@ public class AnthropicApiClientTests : IntegrationTest
|
|||||||
""input_tokens"": 10,
|
""input_tokens"": 10,
|
||||||
""output_tokens"": 25
|
""output_tokens"": 25
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
);
|
);
|
||||||
|
|
||||||
var request = new MessageRequest(
|
var request = new MessageRequest(
|
||||||
@@ -331,12 +331,12 @@ public class AnthropicApiClientTests : IntegrationTest
|
|||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
"application/json",
|
"application/json",
|
||||||
@"{
|
@"{
|
||||||
""type"": ""error"",
|
""type"": ""error"",
|
||||||
""error"": {
|
""error"": {
|
||||||
""type"": ""invalid_request_error"",
|
""type"": ""invalid_request_error"",
|
||||||
""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row""
|
""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row""
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
);
|
);
|
||||||
|
|
||||||
var request = new StreamMessageRequest(
|
var request = new StreamMessageRequest(
|
||||||
@@ -385,7 +385,7 @@ public class AnthropicApiClientTests : IntegrationTest
|
|||||||
""input_tokens"": 10,
|
""input_tokens"": 10,
|
||||||
""output_tokens"": 25
|
""output_tokens"": 25
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
);
|
);
|
||||||
|
|
||||||
var request = new MessageRequest(
|
var request = new MessageRequest(
|
||||||
@@ -428,8 +428,8 @@ public class AnthropicApiClientTests : IntegrationTest
|
|||||||
HttpStatusCode.OK,
|
HttpStatusCode.OK,
|
||||||
"application/json",
|
"application/json",
|
||||||
@"{
|
@"{
|
||||||
""input_tokens"": 10
|
""input_tokens"": 10
|
||||||
}"
|
}"
|
||||||
);
|
);
|
||||||
|
|
||||||
var request = new CountMessageTokensRequest(
|
var request = new CountMessageTokensRequest(
|
||||||
@@ -455,12 +455,12 @@ public class AnthropicApiClientTests : IntegrationTest
|
|||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
"application/json",
|
"application/json",
|
||||||
@"{
|
@"{
|
||||||
""type"": ""error"",
|
""type"": ""error"",
|
||||||
""error"": {
|
""error"": {
|
||||||
""type"": ""invalid_request_error"",
|
""type"": ""invalid_request_error"",
|
||||||
""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row""
|
""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row""
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
);
|
);
|
||||||
|
|
||||||
var request = new CountMessageTokensRequest(
|
var request = new CountMessageTokensRequest(
|
||||||
@@ -479,7 +479,7 @@ public class AnthropicApiClientTests : IntegrationTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CountMessageTokensAsync_WhenCalledRequestFailsAndCanNotSerializeError_ItShouldReturnUnknownError()
|
public async Task CountMessageTokensAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError()
|
||||||
{
|
{
|
||||||
_mockHttpMessageHandler
|
_mockHttpMessageHandler
|
||||||
.WhenCountMessageTokensRequest()
|
.WhenCountMessageTokensRequest()
|
||||||
@@ -503,4 +503,479 @@ public class AnthropicApiClientTests : IntegrationTest
|
|||||||
result.Error.Should().BeOfType<AnthropicError>();
|
result.Error.Should().BeOfType<AnthropicError>();
|
||||||
result.Error.Error.Should().BeOfType<ApiError>();
|
result.Error.Error.Should().BeOfType<ApiError>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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<Page<AnthropicModel>>();
|
||||||
|
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<string, string>
|
||||||
|
{
|
||||||
|
{ "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<Page<AnthropicModel>>();
|
||||||
|
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<Page<AnthropicModel>>();
|
||||||
|
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<AnthropicError>();
|
||||||
|
result.Error.Error.Should().BeOfType<InvalidRequestError>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListModelsAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError()
|
||||||
|
{
|
||||||
|
_mockHttpMessageHandler
|
||||||
|
.WhenListModelsRequest()
|
||||||
|
.Respond(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
"application/json",
|
||||||
|
@"{}"
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = await Client.ListModelsAsync();
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeFalse();
|
||||||
|
result.Error.Should().BeOfType<AnthropicError>();
|
||||||
|
result.Error.Error.Should().BeOfType<ApiError>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListAllModelsAsync_WhenCalled_ItShouldReturnAllModels()
|
||||||
|
{
|
||||||
|
_mockHttpMessageHandler
|
||||||
|
.WhenListModelsRequest()
|
||||||
|
.WithExactQueryString(new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "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<string, string>()
|
||||||
|
{
|
||||||
|
{ "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<Page<AnthropicModel>>();
|
||||||
|
|
||||||
|
await foreach (var response in pageResponses)
|
||||||
|
{
|
||||||
|
response.IsSuccess.Should().BeTrue();
|
||||||
|
response.Value.Should().BeOfType<Page<AnthropicModel>>();
|
||||||
|
collectedPages.Add(response.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
collectedPages.Should().HaveCount(2);
|
||||||
|
collectedPages.Should().BeEquivalentTo(new Page<AnthropicModel>[]
|
||||||
|
{
|
||||||
|
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<AnthropicError>();
|
||||||
|
page.Error.Error.Should().BeOfType<InvalidRequestError>();
|
||||||
|
}
|
||||||
|
|
||||||
|
count.Should().Be(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListAllModelsAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError()
|
||||||
|
{
|
||||||
|
_mockHttpMessageHandler
|
||||||
|
.WhenListModelsRequest()
|
||||||
|
.Respond(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
"application/json",
|
||||||
|
@"{}"
|
||||||
|
);
|
||||||
|
|
||||||
|
var responses = Client.ListAllModelsAsync();
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
await foreach (var page in responses)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
page.IsSuccess.Should().BeFalse();
|
||||||
|
page.Error.Should().BeOfType<AnthropicError>();
|
||||||
|
page.Error.Error.Should().BeOfType<ApiError>();
|
||||||
|
}
|
||||||
|
|
||||||
|
count.Should().Be(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListAllModelsAsync_WhenFirstPageSucceedsAndSecondPageFails_ItShouldReturnFirstPageAndError()
|
||||||
|
{
|
||||||
|
_mockHttpMessageHandler
|
||||||
|
.WhenListModelsRequest()
|
||||||
|
.WithExactQueryString(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "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<string, string>
|
||||||
|
{
|
||||||
|
{ "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<Page<AnthropicModel>>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
page.IsSuccess.Should().BeFalse();
|
||||||
|
page.Error.Should().BeOfType<AnthropicError>();
|
||||||
|
page.Error.Error.Should().BeOfType<InvalidRequestError>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<AnthropicModel>();
|
||||||
|
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<AnthropicError>();
|
||||||
|
result.Error.Error.Should().BeOfType<InvalidRequestError>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<AnthropicError>();
|
||||||
|
result.Error.Error.Should().BeOfType<ApiError>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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().BeTrue();
|
||||||
|
result.Value.Should().BeOfType<AnthropicModel>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ public static class MockHttpMessageHandlerExtensions
|
|||||||
private const string BaseUrl = "https://api.anthropic.com/v1";
|
private const string BaseUrl = "https://api.anthropic.com/v1";
|
||||||
private static readonly string MessagesEndpoint = $"{BaseUrl}/messages";
|
private static readonly string MessagesEndpoint = $"{BaseUrl}/messages";
|
||||||
private static readonly string CountTokensEndpoint = $"{BaseUrl}/messages/count_tokens";
|
private static readonly string CountTokensEndpoint = $"{BaseUrl}/messages/count_tokens";
|
||||||
|
private static readonly string ModelsEndpoint = $"{BaseUrl}/models";
|
||||||
|
|
||||||
private static MockedRequest SetupBaseRequest(
|
private static MockedRequest SetupBaseRequest(
|
||||||
this MockHttpMessageHandler mockHttpMessageHandler,
|
this MockHttpMessageHandler mockHttpMessageHandler,
|
||||||
@@ -51,4 +52,16 @@ public static class MockHttpMessageHandlerExtensions
|
|||||||
return mockHttpMessageHandler
|
return mockHttpMessageHandler
|
||||||
.SetupBaseRequest(HttpMethod.Post, CountTokensEndpoint);
|
.SetupBaseRequest(HttpMethod.Post, CountTokensEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static MockedRequest WhenListModelsRequest(this MockHttpMessageHandler mockHttpMessageHandler)
|
||||||
|
{
|
||||||
|
return mockHttpMessageHandler
|
||||||
|
.SetupBaseRequest(HttpMethod.Get, ModelsEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MockedRequest WhenGetModelRequest(this MockHttpMessageHandler mockHttpMessageHandler, string modelId)
|
||||||
|
{
|
||||||
|
return mockHttpMessageHandler
|
||||||
|
.SetupBaseRequest(HttpMethod.Get, $"{ModelsEndpoint}/{modelId}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
namespace AnthropicClient.Tests.Unit.Models;
|
||||||
|
|
||||||
|
public class AnthropicModelTests : SerializationTest
|
||||||
|
{
|
||||||
|
private const string SampleJson = @"{
|
||||||
|
""type"": ""model"",
|
||||||
|
""id"": ""claude-3-opus-20240229"",
|
||||||
|
""display_name"": ""Claude 3 Opus"",
|
||||||
|
""created_at"": ""2024-02-29T00:00:00Z""
|
||||||
|
}";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet()
|
||||||
|
{
|
||||||
|
var model = new AnthropicModel();
|
||||||
|
|
||||||
|
model.Type.Should().BeEmpty();
|
||||||
|
model.Id.Should().BeEmpty();
|
||||||
|
model.DisplayName.Should().BeEmpty();
|
||||||
|
model.CreatedAt.Should().Be(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues()
|
||||||
|
{
|
||||||
|
var result = Deserialize<AnthropicModel>(SampleJson);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result!.Type.Should().Be("model");
|
||||||
|
result.Id.Should().Be("claude-3-opus-20240229");
|
||||||
|
result.DisplayName.Should().Be("Claude 3 Opus");
|
||||||
|
result.CreatedAt.Should().Be(new DateTimeOffset(2024, 2, 29, 0, 0, 0, TimeSpan.Zero));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonSerialization_WhenSerialized_ItShouldReturnExpectedShape()
|
||||||
|
{
|
||||||
|
var model = new AnthropicModel
|
||||||
|
{
|
||||||
|
Type = "model",
|
||||||
|
Id = "claude-3-opus-20240229",
|
||||||
|
DisplayName = "Claude 3 Opus",
|
||||||
|
CreatedAt = new DateTimeOffset(2024, 2, 29, 0, 0, 0, TimeSpan.Zero)
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = Serialize(model);
|
||||||
|
|
||||||
|
var expectedJson = @"{
|
||||||
|
""type"": ""model"",
|
||||||
|
""id"": ""claude-3-opus-20240229"",
|
||||||
|
""display_name"": ""Claude 3 Opus"",
|
||||||
|
""created_at"": ""2024-02-29T00:00:00+00:00""
|
||||||
|
}";
|
||||||
|
|
||||||
|
JsonAssert.Equal(expectedJson, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
namespace AnthropicClient.Tests.Unit.Models;
|
||||||
|
|
||||||
|
public class PageTests : SerializationTest
|
||||||
|
{
|
||||||
|
private const string BasePageJson = @"{
|
||||||
|
""first_id"": ""msg_123"",
|
||||||
|
""last_id"": ""msg_456"",
|
||||||
|
""has_more"": true
|
||||||
|
}";
|
||||||
|
|
||||||
|
private const string GenericPageJson = @"{
|
||||||
|
""first_id"": ""msg_123"",
|
||||||
|
""last_id"": ""msg_456"",
|
||||||
|
""has_more"": true,
|
||||||
|
""data"": [""item1"", ""item2""]
|
||||||
|
}";
|
||||||
|
|
||||||
|
private const string EmptyPageJson = @"{
|
||||||
|
""first_id"": """",
|
||||||
|
""last_id"": """",
|
||||||
|
""has_more"": false,
|
||||||
|
""data"": []
|
||||||
|
}";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalled_ItShouldReturnAnInstanceWithPropertiesSet()
|
||||||
|
{
|
||||||
|
var page = new Page();
|
||||||
|
|
||||||
|
page.FirstId.Should().BeEmpty();
|
||||||
|
page.LastId.Should().BeEmpty();
|
||||||
|
page.HasMore.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalledWithGeneric_ItShouldReturnAnInstanceWithPropertiesSet()
|
||||||
|
{
|
||||||
|
var page = new Page<string>();
|
||||||
|
|
||||||
|
page.FirstId.Should().BeEmpty();
|
||||||
|
page.LastId.Should().BeEmpty();
|
||||||
|
page.HasMore.Should().BeFalse();
|
||||||
|
page.Data.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonDeserialization_WhenCalled_ItShouldHaveCorrectValues()
|
||||||
|
{
|
||||||
|
var result = Deserialize<Page>(BasePageJson);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result!.FirstId.Should().Be("msg_123");
|
||||||
|
result.LastId.Should().Be("msg_456");
|
||||||
|
result.HasMore.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonSerialization_WhenCalled_ItShouldHaveExpectedShape()
|
||||||
|
{
|
||||||
|
var page = new Page
|
||||||
|
{
|
||||||
|
FirstId = "msg_123",
|
||||||
|
LastId = "msg_456",
|
||||||
|
HasMore = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = Serialize(page);
|
||||||
|
|
||||||
|
JsonAssert.Equal(BasePageJson, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonDeserialization_WhenCalledWithData_ItShouldHaveCorrectValues()
|
||||||
|
{
|
||||||
|
var result = Deserialize<Page<string>>(GenericPageJson);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result!.FirstId.Should().Be("msg_123");
|
||||||
|
result.LastId.Should().Be("msg_456");
|
||||||
|
result.HasMore.Should().BeTrue();
|
||||||
|
result.Data.Should().BeEquivalentTo(["item1", "item2"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonSerialization_WhenCalledWithData_ItShouldHaveExpectedShape()
|
||||||
|
{
|
||||||
|
var page = new Page<string>
|
||||||
|
{
|
||||||
|
FirstId = "msg_123",
|
||||||
|
LastId = "msg_456",
|
||||||
|
HasMore = true,
|
||||||
|
Data = ["item1", "item2"]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = Serialize(page);
|
||||||
|
|
||||||
|
JsonAssert.Equal(GenericPageJson, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonDeserialization_WhenCalledWithEmptyData_ItShouldHaveCorrectValues()
|
||||||
|
{
|
||||||
|
var result = Deserialize<Page<string>>(EmptyPageJson);
|
||||||
|
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result!.FirstId.Should().BeEmpty();
|
||||||
|
result.LastId.Should().BeEmpty();
|
||||||
|
result.HasMore.Should().BeFalse();
|
||||||
|
result.Data.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
namespace AnthropicClient.Tests.Unit.Models;
|
||||||
|
|
||||||
|
public class PagingRequestTests : SerializationTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenLimitIsLessThanMinimum_ItShouldThrowArgumentOutOfRangeException()
|
||||||
|
{
|
||||||
|
var act = () => new PagingRequest(limit: 0);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentOutOfRangeException>().WithMessage("limit must be between 1 and 1000. (Parameter 'limit')");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenLimitIsGreaterThanMaximum_ItShouldThrowArgumentOutOfRangeException()
|
||||||
|
{
|
||||||
|
var act = () => new PagingRequest(limit: 1001);
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentOutOfRangeException>().WithMessage("limit must be between 1 and 1000. (Parameter 'limit')");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenBothBeforeIdAndAfterIdAreSet_ItShouldThrowArgumentException()
|
||||||
|
{
|
||||||
|
var act = () => new PagingRequest(beforeId: "before-id", afterId: "after-id");
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentException>().WithMessage("Only one of beforeId or afterId can be set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToQueryParameters_WhenNoPropertiesSet_ItShouldReturnEmptyString()
|
||||||
|
{
|
||||||
|
var pagingRequest = new PagingRequest();
|
||||||
|
|
||||||
|
var result = pagingRequest.ToQueryParameters();
|
||||||
|
|
||||||
|
result.Should().Be("limit=20");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToQueryParameters_WhenBeforeIdIsSet_ItShouldReturnBeforeId()
|
||||||
|
{
|
||||||
|
var pagingRequest = new PagingRequest(beforeId: "before-id");
|
||||||
|
|
||||||
|
var result = pagingRequest.ToQueryParameters();
|
||||||
|
|
||||||
|
result.Should().Be("before_id=before-id&limit=20");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToQueryParameters_WhenAfterIdIsSet_ItShouldReturnAfterId()
|
||||||
|
{
|
||||||
|
var pagingRequest = new PagingRequest(afterId: "after-id");
|
||||||
|
|
||||||
|
var result = pagingRequest.ToQueryParameters();
|
||||||
|
|
||||||
|
result.Should().Be("after_id=after-id&limit=20");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToQueryParameters_WhenLimitIsSetToNonDefaultValue_ItShouldReturnLimit()
|
||||||
|
{
|
||||||
|
var pagingRequest = new PagingRequest(limit: 10);
|
||||||
|
|
||||||
|
var result = pagingRequest.ToQueryParameters();
|
||||||
|
|
||||||
|
result.Should().Be("limit=10");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user