Merge pull request #30 from StevanFreeborn/stevanfreeborn/feat/add-support-for-models-API

feat: add support for models API
This commit is contained in:
Stevan Freeborn
2025-01-05 22:03:17 -06:00
committed by GitHub
12 changed files with 1106 additions and 23 deletions
+105 -4
View File
@@ -1,6 +1,7 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using AnthropicClient.Json;
using AnthropicClient.Models;
@@ -33,6 +34,28 @@ public interface IAnthropicApiClient
/// <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>
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"/>
@@ -42,6 +65,7 @@ public class AnthropicApiClient : IAnthropicApiClient
private const string ApiKeyHeader = "x-api-key";
private const string MessagesEndpoint = "messages";
private const string CountTokensEndpoint = "messages/count_tokens";
private const string ModelsEndpoint = "models";
private const string JsonContentType = "application/json";
private const string EventPrefix = "event:";
private const string DataPrefix = "data:";
@@ -76,7 +100,7 @@ public class AnthropicApiClient : IAnthropicApiClient
}
}
/// <inheritdoc />
/// <inheritdoc/>
public async Task<AnthropicResult<MessageResponse>> CreateMessageAsync(MessageRequest request)
{
var response = await SendRequestAsync(MessagesEndpoint, request);
@@ -99,7 +123,7 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult<MessageResponse>.Success(msgResponse, anthropicHeaders);
}
/// <inheritdoc />
/// <inheritdoc/>
public async IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRequest request)
{
var response = await SendRequestAsync(MessagesEndpoint, request);
@@ -263,7 +287,7 @@ public class AnthropicApiClient : IAnthropicApiClient
} while (true);
}
/// <inheritdoc />
/// <inheritdoc/>
public async Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request)
{
var response = await SendRequestAsync(CountTokensEndpoint, request);
@@ -277,10 +301,82 @@ public class AnthropicApiClient : IAnthropicApiClient
}
var msgResponse = Deserialize<TokenCountResponse>(responseContent) ?? new TokenCountResponse();
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)
{
var toolUse = response.Content.OfType<ToolUseContent>().FirstOrDefault();
@@ -300,6 +396,11 @@ public class AnthropicApiClient : IAnthropicApiClient
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)
{
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; }
}
+38
View File
@@ -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);
}
}