Skip to content

Commit 0dd7829

Browse files
Merge pull request #30 from StevanFreeborn/stevanfreeborn/feat/add-support-for-models-API
feat: add support for models API
2 parents 4a7d2c3 + 0b81bfc commit 0dd7829

File tree

12 files changed

+1106
-23
lines changed

12 files changed

+1106
-23
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"editor.formatOnSave": true,
23
"dotnet.defaultSolution": "AnthropicClient.sln",
34
"cSpell.words": [
45
"Browsable",

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,73 @@ if (response.IsFailure)
134134
Console.WriteLine("Token Count: {0}", response.Value.InputTokens);
135135
```
136136

137+
### List Models
138+
139+
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.
140+
141+
```csharp
142+
using AnthropicClient;
143+
144+
var response = await client.ListModelsAsync();
145+
146+
if (response.IsFailure)
147+
{
148+
Console.WriteLine("Failed to list models");
149+
Console.WriteLine("Error Type: {0}", response.Error.Error.Type);
150+
Console.WriteLine("Error Message: {0}", response.Error.Error.Message);
151+
return;
152+
}
153+
154+
foreach (var model in response.Value.Data)
155+
{
156+
Console.WriteLine("Model Id: {0}", model.Id);
157+
Console.WriteLine("Model Name: {0}", model.DisplayName);
158+
}
159+
```
160+
161+
Using the `PagingRequest` instance allows you to specify the number of models to return and the page of models to return.
162+
163+
```csharp
164+
using AnthropicClient;
165+
using AnthropicClient.Models;
166+
167+
var response = await client.ListModelsAsync(new PagingRequest(afterId: "claude-3-5-sonnet-20241022", limit: 2));
168+
169+
if (response.IsFailure)
170+
{
171+
Console.WriteLine("Failed to list models");
172+
Console.WriteLine("Error Type: {0}", response.Error.Error.Type);
173+
Console.WriteLine("Error Message: {0}", response.Error.Error.Message);
174+
return;
175+
}
176+
177+
foreach (var model in response.Value.Data)
178+
{
179+
Console.WriteLine("Model Id: {0}", model.Id);
180+
Console.WriteLine("Model Name: {0}", model.DisplayName);
181+
}
182+
```
183+
184+
### Get Model
185+
186+
The `AnthropicApiClient` exposes a method named `GetModelAsync` that can be used to get a model by its id.
187+
188+
```csharp
189+
using AnthropicClient;
190+
191+
var response = await client.GetModelAsync("claude-3-5-sonnet-20241022");
192+
193+
if (response.IsFailure)
194+
{
195+
Console.WriteLine("Failed to get model");
196+
Console.WriteLine("Error Type: {0}", response.Error.Error.Type);
197+
Console.WriteLine("Error Message: {0}", response.Error.Error.Message);
198+
return;
199+
}
200+
201+
Console.WriteLine("Model Id: {0}", response.Value.Id);
202+
```
203+
137204
### Create a message
138205

139206
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.

src/AnthropicClient/AnthropicApiClient.cs

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Net.Http.Headers;
22
using System.Text;
33
using System.Text.Json;
4+
using System.Threading.Tasks;
45

56
using AnthropicClient.Json;
67
using AnthropicClient.Models;
@@ -33,6 +34,28 @@ public interface IAnthropicApiClient
3334
/// <param name="request">The count message tokens request.</param>
3435
/// <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>
3536
Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request);
37+
38+
/// <summary>
39+
/// Lists the models asynchronously.
40+
/// </summary>
41+
/// <param name="request">The paging request to use for listing the models.</param>
42+
/// <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>
43+
Task<AnthropicResult<Page<AnthropicModel>>> ListModelsAsync(PagingRequest? request = null);
44+
45+
/// <summary>
46+
/// Lists the models asynchronously
47+
/// </summary>
48+
/// <param name="limit">The maximum number of models to return in each page.</param>
49+
/// <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>
50+
///
51+
IAsyncEnumerable<AnthropicResult<Page<AnthropicModel>>> ListAllModelsAsync(int limit = 20);
52+
53+
/// <summary>
54+
/// Gets a model by its ID asynchronously.
55+
/// </summary>
56+
/// <param name="modelId">The ID of the model to get.</param>
57+
/// <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>
58+
Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId);
3659
}
3760

3861
/// <inheritdoc cref="IAnthropicApiClient"/>
@@ -42,6 +65,7 @@ public class AnthropicApiClient : IAnthropicApiClient
4265
private const string ApiKeyHeader = "x-api-key";
4366
private const string MessagesEndpoint = "messages";
4467
private const string CountTokensEndpoint = "messages/count_tokens";
68+
private const string ModelsEndpoint = "models";
4569
private const string JsonContentType = "application/json";
4670
private const string EventPrefix = "event:";
4771
private const string DataPrefix = "data:";
@@ -76,7 +100,7 @@ public AnthropicApiClient(string apiKey, HttpClient httpClient)
76100
}
77101
}
78102

79-
/// <inheritdoc />
103+
/// <inheritdoc/>
80104
public async Task<AnthropicResult<MessageResponse>> CreateMessageAsync(MessageRequest request)
81105
{
82106
var response = await SendRequestAsync(MessagesEndpoint, request);
@@ -99,7 +123,7 @@ public async Task<AnthropicResult<MessageResponse>> CreateMessageAsync(MessageRe
99123
return AnthropicResult<MessageResponse>.Success(msgResponse, anthropicHeaders);
100124
}
101125

102-
/// <inheritdoc />
126+
/// <inheritdoc/>
103127
public async IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRequest request)
104128
{
105129
var response = await SendRequestAsync(MessagesEndpoint, request);
@@ -263,7 +287,7 @@ msgResponse is not null
263287
} while (true);
264288
}
265289

266-
/// <inheritdoc />
290+
/// <inheritdoc/>
267291
public async Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request)
268292
{
269293
var response = await SendRequestAsync(CountTokensEndpoint, request);
@@ -277,10 +301,82 @@ public async Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(C
277301
}
278302

279303
var msgResponse = Deserialize<TokenCountResponse>(responseContent) ?? new TokenCountResponse();
280-
281304
return AnthropicResult<TokenCountResponse>.Success(msgResponse, anthropicHeaders);
282305
}
283306

307+
/// <inheritdoc/>
308+
public async Task<AnthropicResult<Page<AnthropicModel>>> ListModelsAsync(PagingRequest? request = null)
309+
{
310+
var pagingRequest = request ?? new PagingRequest();
311+
var endpoint = $"{ModelsEndpoint}?{pagingRequest.ToQueryParameters()}";
312+
var response = await SendRequestAsync(endpoint);
313+
var anthropicHeaders = new AnthropicHeaders(response.Headers);
314+
var responseContent = await response.Content.ReadAsStringAsync();
315+
316+
if (response.IsSuccessStatusCode is false)
317+
{
318+
var error = Deserialize<AnthropicError>(responseContent) ?? new AnthropicError();
319+
return AnthropicResult<Page<AnthropicModel>>.Failure(error, anthropicHeaders);
320+
}
321+
322+
var page = Deserialize<Page<AnthropicModel>>(responseContent) ?? new Page<AnthropicModel>();
323+
return AnthropicResult<Page<AnthropicModel>>.Success(page, anthropicHeaders);
324+
}
325+
326+
/// <inheritdoc/>
327+
public async IAsyncEnumerable<AnthropicResult<Page<AnthropicModel>>> ListAllModelsAsync(int limit = 20)
328+
{
329+
var pagingRequest = new PagingRequest(limit: limit);
330+
string Endpoint() => $"{ModelsEndpoint}?{pagingRequest.ToQueryParameters()}";
331+
bool hasMore;
332+
333+
do
334+
{
335+
var response = await SendRequestAsync(Endpoint());
336+
var anthropicHeaders = new AnthropicHeaders(response.Headers);
337+
var responseContent = await response.Content.ReadAsStringAsync();
338+
339+
if (response.IsSuccessStatusCode is false)
340+
{
341+
var error = Deserialize<AnthropicError>(responseContent) ?? new AnthropicError();
342+
yield return AnthropicResult<Page<AnthropicModel>>.Failure(error, anthropicHeaders);
343+
yield break;
344+
}
345+
346+
var page = Deserialize<Page<AnthropicModel>>(responseContent) ?? new Page<AnthropicModel>();
347+
348+
if (page.HasMore && page.LastId is not null)
349+
{
350+
hasMore = true;
351+
pagingRequest = new PagingRequest(limit: limit, afterId: page.LastId);
352+
}
353+
else
354+
{
355+
hasMore = false;
356+
}
357+
358+
yield return AnthropicResult<Page<AnthropicModel>>.Success(page, anthropicHeaders);
359+
} while (hasMore);
360+
}
361+
362+
/// <inheritdoc/>
363+
public async Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId)
364+
{
365+
var endpoint = $"{ModelsEndpoint}/{modelId}";
366+
var response = await SendRequestAsync(endpoint);
367+
var anthropicHeaders = new AnthropicHeaders(response.Headers);
368+
var responseContent = await response.Content.ReadAsStringAsync();
369+
370+
if (response.IsSuccessStatusCode is false)
371+
{
372+
var error = Deserialize<AnthropicError>(responseContent) ?? new AnthropicError();
373+
return AnthropicResult<AnthropicModel>.Failure(error, anthropicHeaders);
374+
}
375+
376+
var model = Deserialize<AnthropicModel>(responseContent) ?? new AnthropicModel();
377+
return AnthropicResult<AnthropicModel>.Success(model, anthropicHeaders);
378+
}
379+
284380
private ToolCall? GetToolCall(MessageResponse response, List<Tool> tools)
285381
{
286382
var toolUse = response.Content.OfType<ToolUseContent>().FirstOrDefault();
@@ -300,6 +396,11 @@ public async Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(C
300396
return new ToolCall(tool, toolUse);
301397
}
302398

399+
private async Task<HttpResponseMessage> SendRequestAsync(string endpoint)
400+
{
401+
return await _httpClient.GetAsync(endpoint);
402+
}
403+
303404
private async Task<HttpResponseMessage> SendRequestAsync<T>(string endpoint, T request)
304405
{
305406
var requestJson = Serialize(request);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace AnthropicClient.Models;
4+
5+
/// <summary>
6+
/// Represents an Anthropic model.
7+
/// </summary>
8+
public class AnthropicModel
9+
{
10+
/// <summary>
11+
/// The type of the model.
12+
/// </summary>
13+
public string Type { get; init; } = string.Empty;
14+
15+
/// <summary>
16+
/// The id of the model.
17+
/// </summary>
18+
public string Id { get; init; } = string.Empty;
19+
20+
/// <summary>
21+
/// The display name of the model.
22+
/// </summary>
23+
[JsonPropertyName("display_name")]
24+
public string DisplayName { get; init; } = string.Empty;
25+
26+
/// <summary>
27+
/// The created date of the model.
28+
/// </summary>
29+
[JsonPropertyName("created_at")]
30+
public DateTimeOffset CreatedAt { get; init; }
31+
}

src/AnthropicClient/Models/Page.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace AnthropicClient.Models;
4+
5+
/// <summary>
6+
/// Represents a page.
7+
/// </summary>
8+
public class Page
9+
{
10+
/// <summary>
11+
/// The id of the first item in the page.
12+
/// </summary>
13+
[JsonPropertyName("first_id")]
14+
public string? FirstId { get; init; } = string.Empty;
15+
16+
/// <summary>
17+
/// The id of the last item in the page.
18+
/// </summary>
19+
[JsonPropertyName("last_id")]
20+
public string? LastId { get; init; } = string.Empty;
21+
22+
/// <summary>
23+
/// Indicates whether there is more data to be retrieved.
24+
/// </summary>
25+
[JsonPropertyName("has_more")]
26+
public bool HasMore { get; init; }
27+
}
28+
29+
/// <summary>
30+
/// Represents a page with data.
31+
/// </summary>
32+
public class Page<T> : Page
33+
{
34+
/// <summary>
35+
/// The data in the page.
36+
/// </summary>
37+
public T[] Data { get; init; } = [];
38+
}

0 commit comments

Comments
 (0)