Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for counting tokens #29

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 32 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,38 @@ The primary use case for working with the Anthropic API is to create a message i
> [!NOTE]
> The following examples assume that you have already created an instance of the `AnthropicApiClient` class named `client`. You can also find these snippets in the examples directory.

### Count Message Tokens

The `AnthropicApiClient` exposes a method named `CountMessageTokensAsync` that can be used to count the number of tokens in a message. The method requires a `CountMessageTokensRequest` instance as a parameter.

```csharp
using AnthropicClient;
using AnthropicClient.Models;

var response = await client.CountMessageTokensAsync(new CountMessageTokensRequest(
AnthropicModels.Claude3Haiku,
[
new(
MessageRole.User,
[new TextContent("Please write a haiku about the ocean.")]
)
]
));

if (response.IsFailure)
{
Console.WriteLine("Failed to count message tokens");
Console.WriteLine("Error Type: {0}", response.Error.Error.Type);
Console.WriteLine("Error Message: {0}", response.Error.Error.Message);
return;
}

Console.WriteLine("Token Count: {0}", response.Value.InputTokens);
```

### Create a message

The `AnthropicApiClient` exposes a single 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.

#### Non-Streaming

Expand Down Expand Up @@ -694,22 +723,7 @@ foreach (var content in response.Value.Content)

### Prompt Caching

Anthropic has recently introduced a feature called [Prompt Caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) that allows you to cache all or part of the prompt you send to the model. This can be used to improve the performance of your application by reducing latency and token usage. This feature is covered in depth in [Anthropic's API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching).

> [!NOTE]
> This feature is in beta and requires you to set an `anthropic-beta` header on your requests to use it.
> The value of the header should be `prompt-caching-2024-07-31`.

When using this library you can opt-in to prompt caching by adding the required header to the `HttpClient` instance you provide to the `AnthropicApiClient` constructor.

```csharp
using AnthropicClient;

var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "prompt-caching-2024-07-31");

var client = new AnthropicApiClient(apiKey, httpClient);
```
Anthropic provides a feature called [Prompt Caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) that allows you to cache all or part of the prompt you send to the model. This can be used to improve the performance of your application by reducing latency and token usage. This feature is covered in depth in [Anthropic's API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching).

Prompt caching can be used to cache all parts of the prompt including system messages, user messages, and tools. You should refer to the [Anthropic API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) for specifics on limitations and requirements for using prompt caching. This library aims to make using prompt caching convenient and give you complete control over what parts of the prompt are cached. Currently there is only one type of cache control available - `EphemeralCacheControl`.

Expand Down Expand Up @@ -850,22 +864,7 @@ foreach (var content in response.Value.Content)

### PDF Support

Anthropic has recently introduced a feature called [PDF Support](https://docs.anthropic.com/en/docs/build-with-claude/pdf-support) that allows Claude to support PDF input and understand both text and visual content within documents. . This feature is covered in depth in [Anthropic's API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/pdf-support).

> [!NOTE]
> This feature is in beta and requires you to set an `anthropic-beta` header on your requests to use it.
> The value of the header should be `pdfs-2024-09-25`.

When using this library you can opt-in to PDF support by adding the required header to the `HttpClient` instance you provide to the `AnthropicApiClient` constructor.

```csharp
using AnthropicClient;

var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "pdfs-2024-09-25");

var client = new AnthropicApiClient(apiKey, httpClient);
```
Anthropic provides a feature called [PDF Support](https://docs.anthropic.com/en/docs/build-with-claude/pdf-support) that allows Claude to support PDF input and understand both text and visual content within documents. This feature is covered in depth in [Anthropic's API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/pdf-support).

PDF support can be used to provide a PDF document as input to the model. This can be used to provide additional context to the model or to ask for additional information from the model. This library aims to make using PDF support convenient by allowing you to provide the PDF document you want Anthropic's models to consider for use when creating a message.

Expand All @@ -885,11 +884,6 @@ var request = new MessageRequest(
]
);

var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "pdfs-2024-09-25");

var client = new AnthropicApiClient(apiKey, httpClient);

var response = await client.CreateMessageAsync(request);

if (response.IsSuccess is false)
Expand Down
34 changes: 30 additions & 4 deletions src/AnthropicClient/AnthropicApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ public interface IAnthropicApiClient
/// <param name="request">The message request to create.</param>
/// <returns>An asynchronous enumerable that yields the response event by event.</returns>
IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRequest request);

/// <summary>
/// Counts the tokens in a message asynchronously.
/// </summary>
/// <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);
}

/// <inheritdoc cref="IAnthropicApiClient"/>
Expand All @@ -34,6 +41,7 @@ public class AnthropicApiClient : IAnthropicApiClient
private const string BaseUrl = "https://api.anthropic.com/v1/";
private const string ApiKeyHeader = "x-api-key";
private const string MessagesEndpoint = "messages";
private const string CountTokensEndpoint = "messages/count_tokens";
private const string JsonContentType = "application/json";
private const string EventPrefix = "event:";
private const string DataPrefix = "data:";
Expand Down Expand Up @@ -71,7 +79,7 @@ public AnthropicApiClient(string apiKey, HttpClient httpClient)
/// <inheritdoc />
public async Task<AnthropicResult<MessageResponse>> CreateMessageAsync(MessageRequest request)
{
var response = await SendRequestAsync(request);
var response = await SendRequestAsync(MessagesEndpoint, request);
var anthropicHeaders = new AnthropicHeaders(response.Headers);
var responseContent = await response.Content.ReadAsStringAsync();

Expand All @@ -94,7 +102,7 @@ public async Task<AnthropicResult<MessageResponse>> CreateMessageAsync(MessageRe
/// <inheritdoc />
public async IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRequest request)
{
var response = await SendRequestAsync(request);
var response = await SendRequestAsync(MessagesEndpoint, request);

if (response.IsSuccessStatusCode is false)
{
Expand Down Expand Up @@ -255,6 +263,24 @@ msgResponse is not null
} while (true);
}

/// <inheritdoc />
public async Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request)
{
var response = await SendRequestAsync(CountTokensEndpoint, request);
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<TokenCountResponse>.Failure(error, anthropicHeaders);
}

var msgResponse = Deserialize<TokenCountResponse>(responseContent) ?? new TokenCountResponse();

return AnthropicResult<TokenCountResponse>.Success(msgResponse, anthropicHeaders);
}

private ToolCall? GetToolCall(MessageResponse response, List<Tool> tools)
{
var toolUse = response.Content.OfType<ToolUseContent>().FirstOrDefault();
Expand All @@ -274,11 +300,11 @@ msgResponse is not null
return new ToolCall(tool, toolUse);
}

private async Task<HttpResponseMessage> SendRequestAsync(BaseMessageRequest request)
private async Task<HttpResponseMessage> SendRequestAsync<T>(string endpoint, T request)
{
var requestJson = Serialize(request);
var requestContent = new StringContent(requestJson, Encoding.UTF8, JsonContentType);
return await _httpClient.PostAsync(MessagesEndpoint, requestContent);
return await _httpClient.PostAsync(endpoint, requestContent);
}

private string Serialize<T>(T obj) => JsonSerializer.Serialize(obj, JsonSerializationOptions.DefaultOptions);
Expand Down
17 changes: 0 additions & 17 deletions src/AnthropicClient/Models/AnthropicModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,4 @@ public static class AnthropicModels
/// The Claude 3.5 Haiku model.
/// </summary>
public const string Claude35HaikuLatest = "claude-3-5-haiku-latest";

internal static bool IsValidModel(string modelId) => modelId is
Claude3Opus or
Claude3Opus20241022 or
Claude3OpusLatest or

Claude3Sonnet or
Claude3Sonnet20240229 or
Claude35Sonnet or
Claude35Sonnet20240620 or
Claude35Sonnet20241022 or
Claude35SonnetLatest or

Claude3Haiku or
Claude3Haiku20240307 or
Claude35Haiku20241022 or
Claude35HaikuLatest;
}
6 changes: 0 additions & 6 deletions src/AnthropicClient/Models/BaseMessageRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ internal BaseMessageRequest() { }
/// <param name="stream">A value indicating whether the message should be streamed.</param>
/// <param name="stopSequences">The prompt stop sequences.</param>
/// <param name="systemMessages">The system messages to use for the request.</param>
/// <exception cref="ArgumentException">Thrown when the model ID is invalid.</exception>
/// <exception cref="ArgumentNullException">Thrown when the model or messages is null.</exception>
/// <exception cref="ArgumentException">Thrown when the messages contain no messages.</exception>
/// <exception cref="ArgumentException">Thrown when the max tokens is less than one.</exception>
Expand All @@ -151,11 +150,6 @@ protected BaseMessageRequest(
ArgumentValidator.ThrowIfNull(model, nameof(model));
ArgumentValidator.ThrowIfNull(messages, nameof(messages));

if (AnthropicModels.IsValidModel(model) is false)
{
throw new ArgumentException($"Invalid model ID: {model}");
}

if (messages.Count < 1)
{
throw new ArgumentException("Messages must contain at least one message");
Expand Down
72 changes: 72 additions & 0 deletions src/AnthropicClient/Models/CountMessageTokensRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Text.Json.Serialization;

using AnthropicClient.Utils;

namespace AnthropicClient.Models;

/// <summary>
/// Represents a request to count the number of tokens in a message.
/// </summary>
public class CountMessageTokensRequest
{
/// <summary>
/// Gets the model ID to be used for the request.
/// </summary>
public string Model { get; init; } = string.Empty;

/// <summary>
/// Gets the messages to count the number of tokens in.
/// </summary>
public List<Message> Messages { get; init; } = [];

/// <summary>
/// Gets the tool choice mode to use for the request.
/// </summary>
[JsonPropertyName("tool_choice")]
public ToolChoice? ToolChoice { get; init; } = null;

/// <summary>
/// Gets the tools to use for the request.
/// </summary>
public List<Tool>? Tools { get; init; } = null;

/// <summary>
/// Gets the system prompt to use for the request.
/// </summary>
[JsonPropertyName("system")]
public List<TextContent>? SystemPrompt { get; init; } = null;

/// <summary>
/// Initializes a new instance of the <see cref="CountMessageTokensRequest"/> class.
/// </summary>
/// <param name="model">The model ID to use for the request.</param>
/// <param name="messages">The messages to count the number of tokens in.</param>
/// <param name="toolChoice">The tool choice mode to use for the request.</param>
/// <param name="tools">The tools to use for the request.</param>
/// <param name="systemPrompt">The system prompt to use for the request.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="model"/> or <paramref name="messages"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="messages"/> is empty.</exception>
/// <returns>A new instance of the <see cref="CountMessageTokensRequest"/> class.</returns>
public CountMessageTokensRequest(
string model,
List<Message> messages,
ToolChoice? toolChoice = null,
List<Tool>? tools = null,
List<TextContent>? systemPrompt = null
)
{
ArgumentValidator.ThrowIfNull(model, nameof(model));
ArgumentValidator.ThrowIfNull(messages, nameof(messages));

if (messages.Count < 1)
{
throw new ArgumentException("Messages must contain at least one message");
}

Model = model;
Messages = messages;
ToolChoice = toolChoice;
Tools = tools;
SystemPrompt = systemPrompt;
}
}
1 change: 0 additions & 1 deletion src/AnthropicClient/Models/MessageRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ internal MessageRequest() : base() { }
/// <param name="tools">The tools to use for the request.</param>
/// <param name="stopSequences">The prompt stop sequences.</param>
/// <param name="systemMessages">The system messages to include with the request.</param>
/// <exception cref="ArgumentException">Thrown when the model ID is invalid.</exception>
/// <exception cref="ArgumentNullException">Thrown when the model or messages is null.</exception>
/// <exception cref="ArgumentException">Thrown when the messages contain no messages.</exception>
/// <exception cref="ArgumentException">Thrown when the max tokens is less than one.</exception>
Expand Down
1 change: 0 additions & 1 deletion src/AnthropicClient/Models/StreamMessageRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ internal StreamMessageRequest() : base() { }
/// <param name="tools">The tools to use for the request.</param>
/// <param name="stopSequences">The prompt stop sequences.</param>
/// <param name="systemMessages">The system messages to include with the request.</param>
/// <exception cref="ArgumentException">Thrown when the model ID is invalid.</exception>
/// <exception cref="ArgumentNullException">Thrown when the model or messages is null.</exception>
/// <exception cref="ArgumentException">Thrown when the messages contain no messages.</exception>
/// <exception cref="ArgumentException">Thrown when the max tokens is less than one.</exception>
Expand Down
15 changes: 15 additions & 0 deletions src/AnthropicClient/Models/TokenCountResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace AnthropicClient.Models;

/// <summary>
/// Represents a response to a token count request.
/// </summary>
public class TokenCountResponse
{
/// <summary>
/// The number of input tokens counted.
/// </summary>
[JsonPropertyName("input_tokens")]
public int InputTokens { get; init; }
}
Loading
Loading