Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
838e7ba859 | ||
|
|
dc162aa80b | ||
|
|
afe70a9012 | ||
|
|
c2fa796bd0 | ||
|
|
f1c02ebc8f | ||
|
|
42d6ee4d34 | ||
|
|
539d735e6b | ||
|
|
07457c1808 | ||
|
|
4524d7be88 | ||
|
|
eb0befe62a | ||
|
|
e04599bad9 | ||
|
|
2e1c1118c3 | ||
|
|
cc1d628feb | ||
|
|
f6460e99ee | ||
|
|
e47095e03d | ||
|
|
0c160e998a | ||
|
|
a3c7e90734 | ||
|
|
d458c9ed23 | ||
|
|
0253794235 | ||
|
|
f2f150791e | ||
|
|
364c91080c | ||
|
|
6431e2a113 | ||
|
|
987fbe9742 | ||
|
|
35adac9fa0 | ||
|
|
90e9b9f75e | ||
|
|
900a7a3549 | ||
|
|
66008717e1 | ||
|
|
ed97ea95dc | ||
|
|
c0dec5f7de | ||
|
|
051fa178aa | ||
|
|
03ad94d9f9 |
@@ -1,3 +1,4 @@
|
||||
* text eol=crlf
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.jpg binary
|
||||
Vendored
+1
@@ -2,6 +2,7 @@
|
||||
"dotnet.defaultSolution": "AnthropicClient.sln",
|
||||
"cSpell.words": [
|
||||
"Browsable",
|
||||
"haikus",
|
||||
"nameof",
|
||||
"Szalay",
|
||||
"typeof"
|
||||
|
||||
@@ -431,10 +431,11 @@ if (response.IsSuccess is false)
|
||||
return;
|
||||
}
|
||||
|
||||
messages.Add(new(MessageRole.Assistant, response.Content));
|
||||
|
||||
|
||||
foreach (var content in response.Value.Content)
|
||||
{
|
||||
messages.Add(new(MessageRole.Assistant, [content]));
|
||||
|
||||
switch (content)
|
||||
{
|
||||
case TextContent textContent:
|
||||
@@ -552,10 +553,7 @@ if (response is null)
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var content in response.Content)
|
||||
{
|
||||
messages.Add(new(MessageRole.Assistant, [content]));
|
||||
}
|
||||
messages.Add(new(MessageRole.Assistant, response.Content));
|
||||
|
||||
if (response?.ToolCall is not null)
|
||||
{
|
||||
@@ -610,3 +608,242 @@ foreach (var content in finalResponse.Value.Content)
|
||||
```
|
||||
|
||||
If you do find that you need more control over how exactly provided tools are called and how the result of those tools are returned you can avoid using the `InvokeAsync` method and instead use the `Tool` and `ToolUse` properties of the `ToolCall` instance to implement your own solution.
|
||||
|
||||
### System Prompt
|
||||
|
||||
Anthropic's models support the use of system prompts to provide additional context to the user. This can be used to provide additional information to the user or to ask for additional information from the user. This feature is covered in depth in [Anthropic's API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/system-prompts). This library aims to make using system prompts convenient by allowing you to provide the system prompts you want Anthropic's models to consider for use when creating a message.
|
||||
|
||||
#### System Message
|
||||
|
||||
You can create a system prompt by providing a `string` as the `system` parameter in the `MessageRequest` or `StreamMessageRequest` constructor.
|
||||
|
||||
```csharp
|
||||
using AnthropicClient;
|
||||
using AnthropicClient.Models;
|
||||
|
||||
var response = await client.CreateMessageAsync(new MessageRequest(
|
||||
AnthropicModels.Claude3Haiku,
|
||||
[
|
||||
new(
|
||||
MessageRole.User,
|
||||
[new TextContent("Please write a haiku about the ocean.")]
|
||||
)
|
||||
],
|
||||
system: "You are a internationally renowned poet. You excel at writing haikus.
|
||||
));
|
||||
|
||||
if (response.IsSuccess is false)
|
||||
{
|
||||
Console.WriteLine($"Failed to create message");
|
||||
Console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
|
||||
Console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var content in response.Value.Content)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case TextContent textContent:
|
||||
Console.WriteLine(textContent.Text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### System Messages
|
||||
|
||||
You can create a more complex system prompt by providing a `List<TextContent>` as the `systemMessages` parameter in the `MessageRequest` or `StreamMessageRequest` constructor.
|
||||
|
||||
```csharp
|
||||
using AnthropicClient;
|
||||
using AnthropicClient.Models;
|
||||
|
||||
var response = await client.CreateMessageAsync(new MessageRequest(
|
||||
AnthropicModels.Claude3Haiku,
|
||||
[
|
||||
new(
|
||||
MessageRole.User,
|
||||
[new TextContent("Please write a haiku about the ocean.")]
|
||||
)
|
||||
],
|
||||
systemMessages: [
|
||||
new TextContent("You are a internationally renowned poet. You excel at writing haikus."),
|
||||
new TextContent("You have been asked to write a haiku about the ocean.")
|
||||
]
|
||||
));
|
||||
|
||||
if (response.IsSuccess is false)
|
||||
{
|
||||
Console.WriteLine($"Failed to create message");
|
||||
Console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
|
||||
Console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var content in response.Value.Content)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case TextContent textContent:
|
||||
Console.WriteLine(textContent.Text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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);
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
#### Caching System Messages
|
||||
|
||||
System messages can be cached by providing a `List<TextContent>` as the `systemMessages` parameter in the `MessageRequest` or `StreamMessageRequest` constructor and having one or more of the `TextContent` instances have the `CacheControl` property set.
|
||||
|
||||
```csharp
|
||||
using AnthropicClient;
|
||||
using AnthropicClient.Models;
|
||||
|
||||
var response = await client.CreateMessageAsync(new MessageRequest(
|
||||
AnthropicModels.Claude3Haiku,
|
||||
[
|
||||
new(
|
||||
MessageRole.User,
|
||||
[new TextContent("Please write a haiku about the ocean.")]
|
||||
)
|
||||
],
|
||||
systemMessages: [
|
||||
new TextContent("You are a internationally renowned poet. You excel at writing haikus. Please use the following as examples."),
|
||||
new TextContent(exampleHaikus, new EphemeralCacheControl())
|
||||
]
|
||||
));
|
||||
|
||||
if (response.IsSuccess is false)
|
||||
{
|
||||
Console.WriteLine($"Failed to create message");
|
||||
Console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
|
||||
Console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var content in response.Value.Content)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case TextContent textContent:
|
||||
Console.WriteLine(textContent.Text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Caching User Messages
|
||||
|
||||
User messages can be cached by providing a `List<Content>` as the `messages` parameter in the `MessageRequest` or `StreamMessageRequest` constructor and having one or more of the `Content` instances have the `CacheControl` property set.
|
||||
|
||||
```csharp
|
||||
using AnthropicClient;
|
||||
using AnthropicClient.Models;
|
||||
|
||||
var response = await client.CreateMessageAsync(new MessageRequest(
|
||||
AnthropicModels.Claude3Haiku,
|
||||
[
|
||||
new(
|
||||
MessageRole.User,
|
||||
[
|
||||
new TextContent("Please write a haiku about the ocean. Here are some examples of haikus I like."),
|
||||
new TextContent(exampleHaikus, new EphemeralCacheControl())
|
||||
]
|
||||
),
|
||||
]
|
||||
));
|
||||
|
||||
if (response.IsSuccess is false)
|
||||
{
|
||||
Console.WriteLine($"Failed to create message");
|
||||
Console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
|
||||
Console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var content in response.Value.Content)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case TextContent textContent:
|
||||
Console.WriteLine(textContent.Text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Caching Tools
|
||||
|
||||
Tools can be cached by providing a `List<Tool>` as the `tools` parameter in the `MessageRequest` or `StreamMessageRequest` constructor and having one or more of the `Tool` instances have the `CacheControl` property set. This property can be set after the tool is created manually or by using one of the static methods on the `Tool` class.
|
||||
|
||||
```csharp
|
||||
using AnthropicClient;
|
||||
using AnthropicClient.Models;
|
||||
|
||||
var tool = (string location, string units) => $"The weather in {location} is 72 degrees {units}";
|
||||
|
||||
var getWeatherTool = Tool.CreateFromFunction(
|
||||
"Get Weather",
|
||||
"Get the weather for a location in the specified units",
|
||||
tool,
|
||||
new EphemeralCacheControl()
|
||||
);
|
||||
|
||||
var response = await client.CreateMessageAsync(new MessageRequest(
|
||||
AnthropicModels.Claude3Haiku,
|
||||
[
|
||||
new(
|
||||
MessageRole.User,
|
||||
[new TextContent("What is the weather in New York?")]
|
||||
)
|
||||
],
|
||||
tools: [
|
||||
// Lots of other tools
|
||||
// ...
|
||||
getWeatherTool
|
||||
]
|
||||
));
|
||||
|
||||
if (response.IsSuccess is false)
|
||||
{
|
||||
Console.WriteLine($"Failed to create message");
|
||||
Console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
|
||||
Console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var content in response.Value.Content)
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case TextContent textContent:
|
||||
Console.WriteLine(textContent.Text);
|
||||
break;
|
||||
case ToolUseContent toolUseContent:
|
||||
Console.WriteLine(toolUseContent.Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -157,7 +157,7 @@ Class ImageSource <a class="header-action link-secondary" title="View source" h
|
||||
|
||||
<h3 id="AnthropicClient_Models_ImageSource__ctor_System_String_System_String_" data-uid="AnthropicClient.Models.ImageSource.#ctor(System.String,System.String)">
|
||||
ImageSource(string, string)
|
||||
<a class="header-action link-secondary" title="View source" href="https://github.com/StevanFreeborn/anthropic-client/blob/main/src/AnthropicClient/Models/ImageSource.cs/#L36"><i class="bi bi-code-slash"></i></a>
|
||||
<a class="header-action link-secondary" title="View source" href="https://github.com/StevanFreeborn/anthropic-client/blob/main/src/AnthropicClient/Models/ImageSource.cs/#L41"><i class="bi bi-code-slash"></i></a>
|
||||
</h3>
|
||||
|
||||
<div class="markdown level1 summary"><p>Initializes a new instance of the <a class="xref" href="AnthropicClient.Models.ImageSource.html">ImageSource</a> class.</p>
|
||||
@@ -267,6 +267,38 @@ public string MediaType { get; init; }</code></pre>
|
||||
|
||||
|
||||
|
||||
<a id="AnthropicClient_Models_ImageSource_Type_" data-uid="AnthropicClient.Models.ImageSource.Type*"></a>
|
||||
|
||||
<h3 id="AnthropicClient_Models_ImageSource_Type" data-uid="AnthropicClient.Models.ImageSource.Type">
|
||||
Type
|
||||
<a class="header-action link-secondary" title="View source" href="https://github.com/StevanFreeborn/anthropic-client/blob/main/src/AnthropicClient/Models/ImageSource.cs/#L26"><i class="bi bi-code-slash"></i></a>
|
||||
</h3>
|
||||
|
||||
<div class="markdown level1 summary"><p>Gets the type of encoding of the image data.</p>
|
||||
</div>
|
||||
<div class="markdown level1 conceptual"></div>
|
||||
|
||||
<div class="codewrapper">
|
||||
<pre><code class="lang-csharp hljs">public string Type { get; init; }</code></pre>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h4 class="section">Property Value</h4>
|
||||
<dl class="parameters">
|
||||
<dt><a class="xref" href="https://learn.microsoft.com/dotnet/api/system.string">string</a></dt>
|
||||
<dd></dd>
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</article>
|
||||
|
||||
|
||||
+1
-1
@@ -142,7 +142,7 @@
|
||||
"api/AnthropicClient.Models.ImageSource.html": {
|
||||
"href": "api/AnthropicClient.Models.ImageSource.html",
|
||||
"title": "Class ImageSource | AnthropicClient",
|
||||
"keywords": "Class ImageSource Namespace AnthropicClient.Models Assembly AnthropicClient.dll Represents an image source. public class ImageSource Inheritance object ImageSource Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Constructors ImageSource(string, string) Initializes a new instance of the ImageSource class. public ImageSource(string mediaType, string data) Parameters mediaType string The media type of the image. data string The data of the image. Exceptions ArgumentException Thrown when the media type is invalid. ArgumentNullException Thrown when the media type or data is null. Properties Data Gets the data of the image. public string Data { get; init; } Property Value string MediaType Gets the media type of the image. [JsonPropertyName(\"media_type\")] public string MediaType { get; init; } Property Value string"
|
||||
"keywords": "Class ImageSource Namespace AnthropicClient.Models Assembly AnthropicClient.dll Represents an image source. public class ImageSource Inheritance object ImageSource Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Constructors ImageSource(string, string) Initializes a new instance of the ImageSource class. public ImageSource(string mediaType, string data) Parameters mediaType string The media type of the image. data string The data of the image. Exceptions ArgumentException Thrown when the media type is invalid. ArgumentNullException Thrown when the media type or data is null. Properties Data Gets the data of the image. public string Data { get; init; } Property Value string MediaType Gets the media type of the image. [JsonPropertyName(\"media_type\")] public string MediaType { get; init; } Property Value string Type Gets the type of encoding of the image data. public string Type { get; init; } Property Value string"
|
||||
},
|
||||
"api/AnthropicClient.Models.ImageType.html": {
|
||||
"href": "api/AnthropicClient.Models.ImageType.html",
|
||||
|
||||
@@ -1430,6 +1430,19 @@ references:
|
||||
isSpec: "True"
|
||||
fullName: AnthropicClient.Models.ImageSource.MediaType
|
||||
nameWithType: ImageSource.MediaType
|
||||
- uid: AnthropicClient.Models.ImageSource.Type
|
||||
name: Type
|
||||
href: api/AnthropicClient.Models.ImageSource.html#AnthropicClient_Models_ImageSource_Type
|
||||
commentId: P:AnthropicClient.Models.ImageSource.Type
|
||||
fullName: AnthropicClient.Models.ImageSource.Type
|
||||
nameWithType: ImageSource.Type
|
||||
- uid: AnthropicClient.Models.ImageSource.Type*
|
||||
name: Type
|
||||
href: api/AnthropicClient.Models.ImageSource.html#AnthropicClient_Models_ImageSource_Type_
|
||||
commentId: Overload:AnthropicClient.Models.ImageSource.Type
|
||||
isSpec: "True"
|
||||
fullName: AnthropicClient.Models.ImageSource.Type
|
||||
nameWithType: ImageSource.Type
|
||||
- uid: AnthropicClient.Models.ImageType
|
||||
name: ImageType
|
||||
href: api/AnthropicClient.Models.ImageType.html
|
||||
|
||||
@@ -191,6 +191,8 @@ public class AnthropicApiClient : IAnthropicApiClient
|
||||
{
|
||||
InputTokens = existingUsage.InputTokens + msgDeltaData.Usage.InputTokens,
|
||||
OutputTokens = existingUsage.OutputTokens + msgDeltaData.Usage.OutputTokens,
|
||||
CacheCreationInputTokens = existingUsage.CacheCreationInputTokens + msgDeltaData.Usage.CacheCreationInputTokens,
|
||||
CacheReadInputTokens = existingUsage.CacheReadInputTokens + msgDeltaData.Usage.CacheReadInputTokens,
|
||||
};
|
||||
|
||||
msgResponse = new MessageResponse()
|
||||
@@ -274,7 +276,8 @@ public class AnthropicApiClient : IAnthropicApiClient
|
||||
|
||||
private async Task<HttpResponseMessage> SendRequestAsync(BaseMessageRequest request)
|
||||
{
|
||||
var requestContent = new StringContent(Serialize(request), Encoding.UTF8, JsonContentType);
|
||||
var requestJson = Serialize(request);
|
||||
var requestContent = new StringContent(requestJson, Encoding.UTF8, JsonContentType);
|
||||
return await _httpClient.PostAsync(MessagesEndpoint, requestContent);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<PackageId>AnthropicClient</PackageId>
|
||||
<Version>0.0.4</Version>
|
||||
<Version>0.1.0</Version>
|
||||
<Authors>Stevan Freeborn</Authors>
|
||||
<Description>Anthropic Client Library</Description>
|
||||
<PackageProjectUrl>https://anthropicclient.stevanfreeborn.com/</PackageProjectUrl>
|
||||
|
||||
@@ -2,6 +2,48 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [versionize](https://github.com/versionize/versionize) for commit guidelines.
|
||||
|
||||
<a name="0.1.0"></a>
|
||||
## [0.1.0](https://www.github.com/StevanFreeborn/anthropic-client/releases/tag/v0.1.0) (2024-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
* add constructor to allow setting cache control on tool result content ([a3c7e90](https://www.github.com/StevanFreeborn/anthropic-client/commit/a3c7e9073465a0db3a69927f0b8361e9b670266d))
|
||||
* add type for ephemeral cache control ([35adac9](https://www.github.com/StevanFreeborn/anthropic-client/commit/35adac9fa0f3156c07e1fc4324b8fb2c1b50acd8))
|
||||
* first take of adding caching support ([ed97ea9](https://www.github.com/StevanFreeborn/anthropic-client/commit/ed97ea95dc4a2f0bfb9fadd284aad6fed1d4b7e1))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* give cache control proper json property name ([cc1d628](https://www.github.com/StevanFreeborn/anthropic-client/commit/cc1d628feb482655c0b2b1b4675f98fc47f5f584))
|
||||
* make cache control class abstract ([6431e2a](https://www.github.com/StevanFreeborn/anthropic-client/commit/6431e2a113b9a87a53c4a1ddd2c1ef49f5e3e50d))
|
||||
* make cache control setter public ([d458c9e](https://www.github.com/StevanFreeborn/anthropic-client/commit/d458c9ed23a4f575304d0c16db591c1081d6d118))
|
||||
* make sure to pass cache control to tool constructor ([539d735](https://www.github.com/StevanFreeborn/anthropic-client/commit/539d735e6b244715f1e30c67175bf8f2eb6f5729))
|
||||
* remove converter ([6600871](https://www.github.com/StevanFreeborn/anthropic-client/commit/66008717e1392f6667e2acc0d5016059adc2e491))
|
||||
* reuse validation logic in constructors ([987fbe9](https://www.github.com/StevanFreeborn/anthropic-client/commit/987fbe974236379f788df4fca2f0326901b2c094))
|
||||
* use additional constructor param with default value instead of overloaded constructor to avoid potentially breaking others code with that would then contain ambigious constructor calls. ([90e9b9f](https://www.github.com/StevanFreeborn/anthropic-client/commit/90e9b9f75eec1488277538fff5f90b817ded3b6b))
|
||||
|
||||
### Other
|
||||
|
||||
* Merge pull request #12 from StevanFreeborn/stevanfreeborn/tests/fix-image-test [skip ci] ([c0dec5f](https://www.github.com/StevanFreeborn/anthropic-client/commit/c0dec5f7de35977af52db86e1977cf8529e2f134))
|
||||
* Merge pull request #14 from StevanFreeborn/stevanfreeborn/feat/add-support-for-prompt-caching ([dc162aa](https://www.github.com/StevanFreeborn/anthropic-client/commit/dc162aa80beb3d2a7792b04311933002da82dcaf))
|
||||
* add method for creating client with customized http client ([eb0befe](https://www.github.com/StevanFreeborn/anthropic-client/commit/eb0befe62a1585c7fe26e0e1ff616e27b6dd52ff))
|
||||
* add test for cache control type static class ([364c910](https://www.github.com/StevanFreeborn/anthropic-client/commit/364c91080c463fcb2249dfee37745c8ff20c7586))
|
||||
* add test for serializing system property with correct expected value based on whether given system messages or just a system message. ([f6460e9](https://www.github.com/StevanFreeborn/anthropic-client/commit/f6460e99ee5195342b63b71e4e1a720eaaf4432e))
|
||||
* add test to make sure cache control can be set on tool use content objects ([e47095e](https://www.github.com/StevanFreeborn/anthropic-client/commit/e47095e03dc49d5a3954f37cc928b3eaced04779))
|
||||
* add tests for constructor using cache control ([0c160e9](https://www.github.com/StevanFreeborn/anthropic-client/commit/0c160e998a628af0f21883e592ddc8fb902f170f))
|
||||
* add tests for creating tools with cache control set ([42d6ee4](https://www.github.com/StevanFreeborn/anthropic-client/commit/42d6ee4d3443f7d8a3962a07fd5f7fc6b919e5ff))
|
||||
* add tests for ephemeral cache control model ([f2f1507](https://www.github.com/StevanFreeborn/anthropic-client/commit/f2f150791ec5ba4bced53094bafbc95e8f9ae087))
|
||||
* add tests for overloaded constructor ([0253794](https://www.github.com/StevanFreeborn/anthropic-client/commit/025379423527677d9fab77ca92a5ca4004ea1571))
|
||||
* add text greater than 2048 tokens for testing caching ([2e1c111](https://www.github.com/StevanFreeborn/anthropic-client/commit/2e1c1118c36878cedfde8fee7a645eadfa1ed418))
|
||||
* added end to end tests for cache control when caching system messages, user messages, or tools ([4524d7b](https://www.github.com/StevanFreeborn/anthropic-client/commit/4524d7be8880a3014656ccb0b6b91ec9b76c0c89))
|
||||
* documentation for v0.0.4 [skip ci] ([03ad94d](https://www.github.com/StevanFreeborn/anthropic-client/commit/03ad94d9f96035faae01c0ccba92bac985c1e832))
|
||||
* put request json in intermediate variable to make debugging better ([e04599b](https://www.github.com/StevanFreeborn/anthropic-client/commit/e04599bad9c930d8101ace78ee9c1d234f93e52e))
|
||||
* remove unused using statement ([07457c1](https://www.github.com/StevanFreeborn/anthropic-client/commit/07457c1808fc7b07bc28a5904a7dd6cfbc7053df))
|
||||
* run dotnet format ([afe70a9](https://www.github.com/StevanFreeborn/anthropic-client/commit/afe70a90125ab569e5c5b7dceeba2836807f84b3))
|
||||
* stop git from messing up image file ([051fa17](https://www.github.com/StevanFreeborn/anthropic-client/commit/051fa178aa7d464cc82db0dbcd919260a3f55368))
|
||||
* update README.md with documentation about using prompt caching with this library. ([c2fa796](https://www.github.com/StevanFreeborn/anthropic-client/commit/c2fa796bd013d55d42468980fb9d9ac98f025264))
|
||||
* update tests to account for serialization and deserialization changes with new/modified model properties to support caching ([900a7a3](https://www.github.com/StevanFreeborn/anthropic-client/commit/900a7a354941f6b5a1357cacbafb64cd8220d918))
|
||||
* whitelist word ([f1c02eb](https://www.github.com/StevanFreeborn/anthropic-client/commit/f1c02ebc8faf4804a464d27c2a5d4c663cb82015))
|
||||
|
||||
<a name="0.0.4"></a>
|
||||
## [0.0.4](https://www.github.com/StevanFreeborn/anthropic-client/releases/tag/v0.0.4) (2024-07-20)
|
||||
|
||||
|
||||
@@ -14,11 +14,46 @@ public abstract class BaseMessageRequest
|
||||
/// </summary>
|
||||
public string Model { get; init; } = string.Empty;
|
||||
|
||||
// TODO: I do not like this. I would prefer to have a single property that is a list of TextContent objects.
|
||||
// This approach was taken to maintain compatibility with the API. As someone could be using the System property
|
||||
// and changing it to a list of TextContent objects would break their code.
|
||||
// However if an opportunity arises for a breaking change release, this should be changed.
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system prompt to use for the request.
|
||||
/// Gets the system message that will be used as the system prompt if no system messages are provided.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string? System { get; init; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system messages to send to the model to be used as the system prompt.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public List<TextContent>? SystemMessages { get; init; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system prompt that will be used for the request.
|
||||
/// If will return the system messages if they are provided, otherwise it will return the system message.
|
||||
/// If neither are provided, it will return null.
|
||||
/// </summary>
|
||||
[JsonPropertyName("system")]
|
||||
public List<TextContent>? SystemPrompt => GetSystemPrompt();
|
||||
|
||||
private List<TextContent>? GetSystemPrompt()
|
||||
{
|
||||
if (SystemMessages is not null)
|
||||
{
|
||||
return SystemMessages;
|
||||
}
|
||||
|
||||
if (System is not null)
|
||||
{
|
||||
return [new TextContent(System)];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the messages to send to the model.
|
||||
/// </summary>
|
||||
@@ -81,7 +116,7 @@ public abstract class BaseMessageRequest
|
||||
/// <param name="model">The model ID to use for the request.</param>
|
||||
/// <param name="messages">The messages to send to the model.</param>
|
||||
/// <param name="maxTokens">The maximum number of tokens to generate.</param>
|
||||
/// <param name="system">The system ID to use for the request.</param>
|
||||
/// <param name="system">The system prompt to use for the request.</param>
|
||||
/// <param name="metadata">The metadata to include with the request.</param>
|
||||
/// <param name="temperature">The temperature to use for the request.</param>
|
||||
/// <param name="topK">The top-K value to use for the request.</param>
|
||||
@@ -90,6 +125,7 @@ public abstract class BaseMessageRequest
|
||||
/// <param name="tools">The tools to use for the request.</param>
|
||||
/// <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>
|
||||
@@ -99,16 +135,17 @@ public abstract class BaseMessageRequest
|
||||
protected BaseMessageRequest(
|
||||
string model,
|
||||
List<Message> messages,
|
||||
int maxTokens = 1024,
|
||||
string? system = null,
|
||||
Dictionary<string, object>? metadata = null,
|
||||
decimal temperature = 0.0m,
|
||||
int? topK = null,
|
||||
decimal? topP = null,
|
||||
ToolChoice? toolChoice = null,
|
||||
List<Tool>? tools = null,
|
||||
bool stream = false,
|
||||
List<string>? stopSequences = null
|
||||
int maxTokens,
|
||||
string? system,
|
||||
Dictionary<string, object>? metadata,
|
||||
decimal temperature,
|
||||
int? topK,
|
||||
decimal? topP,
|
||||
ToolChoice? toolChoice,
|
||||
List<Tool>? tools,
|
||||
bool stream,
|
||||
List<string>? stopSequences,
|
||||
List<TextContent>? systemMessages
|
||||
)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(model, nameof(model));
|
||||
@@ -138,6 +175,7 @@ public abstract class BaseMessageRequest
|
||||
Messages = messages;
|
||||
MaxTokens = maxTokens;
|
||||
System = system;
|
||||
SystemMessages = systemMessages;
|
||||
Metadata = metadata;
|
||||
Temperature = temperature;
|
||||
TopK = topK;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using AnthropicClient.Utils;
|
||||
|
||||
namespace AnthropicClient.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the cache control to be used for content.
|
||||
/// </summary>
|
||||
public abstract class CacheControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the type of the cache control.
|
||||
/// </summary>
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CacheControl"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the cache control.</param>
|
||||
/// <returns>A new instance of the <see cref="CacheControl"/> class.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the type is null or whitespace.</exception>
|
||||
protected CacheControl(string type)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNullOrWhitespace(type, nameof(type));
|
||||
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace AnthropicClient.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Provides constants for cache control types.
|
||||
/// </summary>
|
||||
public static class CacheControlType
|
||||
{
|
||||
/// <summary>
|
||||
/// The cache control type for an ephemeral cache.
|
||||
/// </summary>
|
||||
public const string Ephemeral = "ephemeral";
|
||||
}
|
||||
@@ -12,6 +12,12 @@ public abstract class Content
|
||||
/// </summary>
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cache control to be used for the content.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cache_control")]
|
||||
public CacheControl? CacheControl { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
internal Content()
|
||||
{
|
||||
@@ -26,4 +32,16 @@ public abstract class Content
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Content"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the content.</param>
|
||||
/// <param name="cacheControl">The cache control to be used for the content.</param>
|
||||
/// <returns>A new instance of the <see cref="Content"/> class.</returns>
|
||||
protected Content(string type, CacheControl cacheControl)
|
||||
{
|
||||
Type = type;
|
||||
CacheControl = cacheControl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace AnthropicClient.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the cache control to be used for content.
|
||||
/// </summary>
|
||||
public class EphemeralCacheControl : CacheControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EphemeralCacheControl"/> class.
|
||||
/// </summary>
|
||||
/// <returns>A new instance of the <see cref="EphemeralCacheControl"/> class.</returns>
|
||||
public EphemeralCacheControl() : base(CacheControlType.Ephemeral) { }
|
||||
}
|
||||
@@ -19,6 +19,12 @@ public class ImageContent : Content
|
||||
{
|
||||
}
|
||||
|
||||
private void Validate(string mediaType, string data)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(mediaType, nameof(mediaType));
|
||||
ArgumentValidator.ThrowIfNull(data, nameof(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageContent"/> class.
|
||||
/// </summary>
|
||||
@@ -28,8 +34,22 @@ public class ImageContent : Content
|
||||
/// <returns>A new instance of the <see cref="ImageContent"/> class.</returns>
|
||||
public ImageContent(string mediaType, string data) : base(ContentType.Image)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(mediaType, nameof(mediaType));
|
||||
ArgumentValidator.ThrowIfNull(data, nameof(data));
|
||||
Validate(mediaType, data);
|
||||
|
||||
Source = new(mediaType, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageContent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="mediaType">The media type of the image.</param>
|
||||
/// <param name="data">The data of the image.</param>
|
||||
/// <param name="cacheControl">The cache control to be used for the content.</param>
|
||||
/// <returns>A new instance of the <see cref="ImageContent"/> class.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when the media type, data, or cache control is null.</exception>
|
||||
public ImageContent(string mediaType, string data, CacheControl cacheControl) : base(ContentType.Image, cacheControl)
|
||||
{
|
||||
Validate(mediaType, data);
|
||||
|
||||
Source = new(mediaType, data);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class MessageRequest : BaseMessageRequest
|
||||
/// <param name="model">The model ID to use for the request.</param>
|
||||
/// <param name="messages">The messages to send to the model.</param>
|
||||
/// <param name="maxTokens">The maximum number of tokens to generate.</param>
|
||||
/// <param name="system">The system ID to use for the request.</param>
|
||||
/// <param name="system">The system prompt to use for the request.</param>
|
||||
/// <param name="metadata">The metadata to include with the request.</param>
|
||||
/// <param name="temperature">The temperature to use for the request.</param>
|
||||
/// <param name="topK">The top-K value to use for the request.</param>
|
||||
@@ -24,6 +24,7 @@ public class MessageRequest : BaseMessageRequest
|
||||
/// <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="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>
|
||||
@@ -41,7 +42,8 @@ public class MessageRequest : BaseMessageRequest
|
||||
decimal? topP = null,
|
||||
ToolChoice? toolChoice = null,
|
||||
List<Tool>? tools = null,
|
||||
List<string>? stopSequences = null
|
||||
List<string>? stopSequences = null,
|
||||
List<TextContent>? systemMessages = null
|
||||
) : base(
|
||||
model,
|
||||
messages,
|
||||
@@ -54,7 +56,8 @@ public class MessageRequest : BaseMessageRequest
|
||||
toolChoice,
|
||||
tools,
|
||||
false,
|
||||
stopSequences
|
||||
stopSequences,
|
||||
systemMessages
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class StreamMessageRequest : BaseMessageRequest
|
||||
/// <param name="model">The model ID to use for the request.</param>
|
||||
/// <param name="messages">The messages to send to the model.</param>
|
||||
/// <param name="maxTokens">The maximum number of tokens to generate.</param>
|
||||
/// <param name="system">The system ID to use for the request.</param>
|
||||
/// <param name="system">The system prompt to use for the request.</param>
|
||||
/// <param name="metadata">The metadata to include with the request.</param>
|
||||
/// <param name="temperature">The temperature to use for the request.</param>
|
||||
/// <param name="topK">The top-K value to use for the request.</param>
|
||||
@@ -24,6 +24,7 @@ public class StreamMessageRequest : BaseMessageRequest
|
||||
/// <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="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>
|
||||
@@ -41,7 +42,8 @@ public class StreamMessageRequest : BaseMessageRequest
|
||||
decimal? topP = null,
|
||||
ToolChoice? toolChoice = null,
|
||||
List<Tool>? tools = null,
|
||||
List<string>? stopSequences = null
|
||||
List<string>? stopSequences = null,
|
||||
List<TextContent>? systemMessages = null
|
||||
) : base(
|
||||
model,
|
||||
messages,
|
||||
@@ -54,7 +56,8 @@ public class StreamMessageRequest : BaseMessageRequest
|
||||
toolChoice,
|
||||
tools,
|
||||
true,
|
||||
stopSequences
|
||||
stopSequences,
|
||||
systemMessages
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ public class TextContent : Content
|
||||
{
|
||||
}
|
||||
|
||||
private void Validate(string text)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(text, nameof(text));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextContent"/> class.
|
||||
/// </summary>
|
||||
@@ -27,7 +32,21 @@ public class TextContent : Content
|
||||
/// <returns>A new instance of the <see cref="TextContent"/> class.</returns>
|
||||
public TextContent(string text) : base(ContentType.Text)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(text, nameof(text));
|
||||
Validate(text);
|
||||
|
||||
Text = text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextContent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="text">The text of the content.</param>
|
||||
/// <param name="cacheControl">The cache control to be used for the content.</param>
|
||||
/// <returns>A new instance of the <see cref="TextContent"/> class.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when the text or cache control is null.</exception>
|
||||
public TextContent(string text, CacheControl cacheControl) : base(ContentType.Text, cacheControl)
|
||||
{
|
||||
Validate(text);
|
||||
|
||||
Text = text;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,12 @@ public class Tool
|
||||
[JsonIgnore]
|
||||
public AnthropicFunction Function { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cache control to be used for the tool.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cache_control")]
|
||||
public CacheControl? CacheControl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display name of the tool.
|
||||
/// </summary>
|
||||
@@ -73,7 +79,7 @@ public class Tool
|
||||
DisplayName = string.Empty;
|
||||
}
|
||||
|
||||
internal Tool(string name, string description, AnthropicFunction function)
|
||||
internal Tool(string name, string description, AnthropicFunction function, CacheControl? cacheControl = null)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNullOrWhitespace(name, nameof(name));
|
||||
ArgumentValidator.ThrowIfNullOrWhitespace(description, nameof(description));
|
||||
@@ -89,6 +95,7 @@ public class Tool
|
||||
Description = description;
|
||||
Function = function;
|
||||
InputSchema = JsonSchemaGenerator.GenerateInputSchema(function);
|
||||
CacheControl = cacheControl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -99,7 +106,7 @@ public class Tool
|
||||
/// <exception cref="ArgumentNullException">Thrown when the function of the tool is null.</exception>
|
||||
/// <returns>The created tool as instance of <see cref="Tool"/>.</returns>
|
||||
/// <remarks>The implementation of <see cref="ITool"/> must have a parameterless constructor.</remarks>
|
||||
public static Tool CreateFromClass<T>() where T : ITool, new()
|
||||
public static Tool CreateFromClass<T>(CacheControl? cacheControl = null) where T : ITool, new()
|
||||
{
|
||||
var tool = new T();
|
||||
|
||||
@@ -107,7 +114,7 @@ public class Tool
|
||||
ArgumentValidator.ThrowIfNullOrWhitespace(tool.Description, nameof(tool.Description));
|
||||
ArgumentValidator.ThrowIfNull(tool.Function, nameof(tool.Function));
|
||||
|
||||
return new Tool(tool.Name, tool.Description, new AnthropicFunction(tool.Function, tool));
|
||||
return new Tool(tool.Name, tool.Description, new AnthropicFunction(tool.Function, tool), cacheControl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -117,12 +124,19 @@ public class Tool
|
||||
/// <param name="description">The description of the tool.</param>
|
||||
/// <param name="type">The type that contains the method.</param>
|
||||
/// <param name="methodName">The name of the method.</param>
|
||||
/// <param name="cacheControl">The cache control to be used for the tool.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="methodName"/> is null or empty.</exception>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="type"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when the method is not found in the type.</exception>
|
||||
/// <returns>The created tool as instance of <see cref="Tool"/>.</returns>
|
||||
/// <remarks>The name of the tool will be sanitized to conform to the Anthropic tool naming rules.</remarks>
|
||||
public static Tool CreateFromStaticMethod(string name, string description, Type type, string methodName)
|
||||
public static Tool CreateFromStaticMethod(
|
||||
string name,
|
||||
string description,
|
||||
Type type,
|
||||
string methodName,
|
||||
CacheControl? cacheControl = null
|
||||
)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNullOrWhitespace(methodName, nameof(methodName));
|
||||
ArgumentValidator.ThrowIfNull(type, nameof(type));
|
||||
@@ -134,7 +148,7 @@ public class Tool
|
||||
throw new ArgumentException($"Method '{methodName}' not found in type '{type.FullName}'.", nameof(methodName));
|
||||
}
|
||||
|
||||
return new Tool(name, description, new AnthropicFunction(method));
|
||||
return new Tool(name, description, new AnthropicFunction(method), cacheControl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -144,12 +158,19 @@ public class Tool
|
||||
/// <param name="description">The description of the tool.</param>
|
||||
/// <param name="instance">The instance that contains the method.</param>
|
||||
/// <param name="methodName">The name of the method.</param>
|
||||
/// <param name="cacheControl">The cache control to be used for the tool.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="methodName"/> is null or empty.</exception>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="instance"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="methodName"/> is not found in the type of <paramref name="instance"/>.</exception>
|
||||
/// <returns>The created tool as instance of <see cref="Tool"/>.</returns>
|
||||
/// <remarks>The name of the tool will be sanitized to conform to the Anthropic tool naming rules.</remarks>
|
||||
public static Tool CreateFromInstanceMethod(string name, string description, object instance, string methodName)
|
||||
public static Tool CreateFromInstanceMethod(
|
||||
string name,
|
||||
string description,
|
||||
object instance,
|
||||
string methodName,
|
||||
CacheControl? cacheControl = null
|
||||
)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNullOrWhitespace(methodName, nameof(methodName));
|
||||
ArgumentValidator.ThrowIfNull(instance, nameof(instance));
|
||||
@@ -161,7 +182,7 @@ public class Tool
|
||||
throw new ArgumentException($"Method '{methodName}' not found in type '{instance.GetType().FullName}'.", nameof(methodName));
|
||||
}
|
||||
|
||||
return new Tool(name, description, new AnthropicFunction(method, instance));
|
||||
return new Tool(name, description, new AnthropicFunction(method, instance), cacheControl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -171,14 +192,20 @@ public class Tool
|
||||
/// <param name="name">The name of the tool.</param>
|
||||
/// <param name="description">The description of the tool.</param>
|
||||
/// <param name="func">The function.</param>
|
||||
/// <param name="cacheControl">The cache control to be used for the tool.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="func"/> is null.</exception>
|
||||
/// <returns>The created tool as instance of <see cref="Tool"/>.</returns>
|
||||
/// <remarks>The name of the tool will be sanitized to conform to the Anthropic tool naming rules.</remarks>
|
||||
public static Tool CreateFromFunction<TResult>(string name, string description, Func<TResult> func)
|
||||
public static Tool CreateFromFunction<TResult>(
|
||||
string name,
|
||||
string description,
|
||||
Func<TResult> func,
|
||||
CacheControl? cacheControl = null
|
||||
)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(func, nameof(func));
|
||||
|
||||
return new Tool(name, description, new AnthropicFunction(func.Method, func.Target));
|
||||
return new Tool(name, description, new AnthropicFunction(func.Method, func.Target), cacheControl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -189,14 +216,20 @@ public class Tool
|
||||
/// <param name="name">The name of the tool.</param>
|
||||
/// <param name="description">The description of the tool.</param>
|
||||
/// <param name="func">The function.</param>
|
||||
/// <param name="cacheControl">The cache control to be used for the tool.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="func"/> is null.</exception>
|
||||
/// <returns>The created tool as instance of <see cref="Tool"/>.</returns>
|
||||
/// <remarks>The name of the tool will be sanitized to conform to the Anthropic tool naming rules.</remarks>
|
||||
public static Tool CreateFromFunction<T1, TResult>(string name, string description, Func<T1, TResult> func)
|
||||
public static Tool CreateFromFunction<T1, TResult>(
|
||||
string name,
|
||||
string description,
|
||||
Func<T1, TResult> func,
|
||||
CacheControl? cacheControl = null
|
||||
)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(func, nameof(func));
|
||||
|
||||
return new Tool(name, description, new AnthropicFunction(func.Method, func.Target));
|
||||
return new Tool(name, description, new AnthropicFunction(func.Method, func.Target), cacheControl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -208,14 +241,20 @@ public class Tool
|
||||
/// <param name="name">The name of the tool.</param>
|
||||
/// <param name="description">The description of the tool.</param>
|
||||
/// <param name="func">The function.</param>
|
||||
/// <param name="cacheControl">The cache control to be used for the tool.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="func"/> is null.</exception>
|
||||
/// <returns>The created tool as instance of <see cref="Tool"/>.</returns>
|
||||
/// <remarks>The name of the tool will be sanitized to conform to the Anthropic tool naming rules.</remarks>
|
||||
public static Tool CreateFromFunction<T1, T2, TResult>(string name, string description, Func<T1, T2, TResult> func)
|
||||
public static Tool CreateFromFunction<T1, T2, TResult>(
|
||||
string name,
|
||||
string description,
|
||||
Func<T1, T2, TResult> func,
|
||||
CacheControl? cacheControl = null
|
||||
)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(func, nameof(func));
|
||||
|
||||
return new Tool(name, description, new AnthropicFunction(func.Method, func.Target));
|
||||
return new Tool(name, description, new AnthropicFunction(func.Method, func.Target), cacheControl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,12 @@ public class ToolResultContent : Content
|
||||
[JsonConstructor]
|
||||
internal ToolResultContent() : base(ContentType.ToolResult) { }
|
||||
|
||||
private void Validate(string toolUseId, string content)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(toolUseId, nameof(toolUseId));
|
||||
ArgumentValidator.ThrowIfNull(content, nameof(content));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ToolResultContent"/> class.
|
||||
/// </summary>
|
||||
@@ -32,10 +38,26 @@ public class ToolResultContent : Content
|
||||
/// <returns>A new instance of the <see cref="ToolResultContent"/> class.</returns>
|
||||
public ToolResultContent(string toolUseId, string content) : base(ContentType.ToolResult)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(toolUseId, nameof(toolUseId));
|
||||
ArgumentValidator.ThrowIfNull(content, nameof(content));
|
||||
Validate(toolUseId, content);
|
||||
|
||||
ToolUseId = toolUseId;
|
||||
Content = content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ToolResultContent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="toolUseId">The tool use ID of the content.</param>
|
||||
/// <param name="content">The content of the tool result.</param>
|
||||
/// <param name="cacheControl">The cache control to be used for the content.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when the tool use ID or content is null.</exception>
|
||||
/// <returns>A new instance of the <see cref="ToolResultContent"/> class.</returns>
|
||||
public ToolResultContent(string toolUseId, string content, CacheControl cacheControl) : base(ContentType.ToolResult)
|
||||
{
|
||||
Validate(toolUseId, content);
|
||||
|
||||
ToolUseId = toolUseId;
|
||||
Content = content;
|
||||
CacheControl = cacheControl;
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,16 @@ public class Usage
|
||||
/// </summary>
|
||||
[JsonPropertyName("output_tokens")]
|
||||
public int OutputTokens { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of tokens written to the cache when creating a new entry
|
||||
/// </summary>
|
||||
[JsonPropertyName("cache_creation_input_tokens")]
|
||||
public int CacheCreationInputTokens { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of tokens retrieved from the cache for the request.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cache_read_input_tokens")]
|
||||
public int CacheReadInputTokens { get; init; }
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace AnthropicClient.Tests.EndToEnd;
|
||||
|
||||
public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture)
|
||||
{
|
||||
private string GetTestFilePath(string fileName) =>
|
||||
Path.Combine(Directory.GetCurrentDirectory(), "Files", fileName);
|
||||
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse()
|
||||
{
|
||||
@@ -63,9 +63,10 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenImageIsSent_ItShouldReturnResponse()
|
||||
{
|
||||
var imagePath = Path.Combine(Directory.GetCurrentDirectory(), "Files", "base64-elephant.txt");
|
||||
var imagePath = GetTestFilePath("elephant.jpg");
|
||||
var mediaType = "image/jpeg";
|
||||
var base64Data = await File.ReadAllTextAsync(imagePath);
|
||||
var bytes = await File.ReadAllBytesAsync(imagePath);
|
||||
var base64Data = Convert.ToBase64String(bytes);
|
||||
|
||||
var request = new MessageRequest(
|
||||
model: AnthropicModels.Claude3Haiku,
|
||||
@@ -95,4 +96,130 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
|
||||
|
||||
text.Should().Contain("elephant");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenSystemMessagesContainCacheControl_ItShouldUseCache()
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "prompt-caching-2024-07-31");
|
||||
|
||||
var client = CreateClient(httpClient);
|
||||
|
||||
var storyPath = GetTestFilePath("story.txt");
|
||||
var storyText = await File.ReadAllTextAsync(storyPath);
|
||||
|
||||
var request = new MessageRequest(
|
||||
model: AnthropicModels.Claude3Haiku,
|
||||
systemMessages: [
|
||||
new("You are a helpful assistant who can answer questions about the following text:"),
|
||||
new(storyText, new EphemeralCacheControl())
|
||||
],
|
||||
messages: [
|
||||
new(MessageRole.User, [
|
||||
new TextContent("Give me a one sentence summary of this story.")
|
||||
]),
|
||||
]
|
||||
);
|
||||
|
||||
var resultOne = await client.CreateMessageAsync(request);
|
||||
|
||||
resultOne.IsSuccess.Should().BeTrue();
|
||||
resultOne.Value.Should().BeOfType<MessageResponse>();
|
||||
resultOne.Value.Content.Should().NotBeNullOrEmpty();
|
||||
resultOne.Value.Usage.Should().Match<Usage>(u => u.CacheCreationInputTokens > 0 || u.CacheReadInputTokens > 0);
|
||||
|
||||
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
||||
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")]));
|
||||
|
||||
var resultTwo = await client.CreateMessageAsync(request);
|
||||
|
||||
resultTwo.IsSuccess.Should().BeTrue();
|
||||
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
||||
resultTwo.Value.Content.Should().NotBeNullOrEmpty();
|
||||
resultTwo.Value.Usage.CacheReadInputTokens.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenMessagesContainCacheControl_ItShouldUseCache()
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "prompt-caching-2024-07-31");
|
||||
|
||||
var client = CreateClient(httpClient);
|
||||
|
||||
var storyPath = GetTestFilePath("story.txt");
|
||||
var storyText = await File.ReadAllTextAsync(storyPath);
|
||||
|
||||
var request = new MessageRequest(
|
||||
model: AnthropicModels.Claude3Haiku,
|
||||
messages: [
|
||||
new(MessageRole.User, [
|
||||
new TextContent("Give me a one sentence summary of this story."),
|
||||
new TextContent(storyText, new EphemeralCacheControl())
|
||||
]),
|
||||
]
|
||||
);
|
||||
|
||||
var resultOne = await client.CreateMessageAsync(request);
|
||||
|
||||
resultOne.IsSuccess.Should().BeTrue();
|
||||
resultOne.Value.Should().BeOfType<MessageResponse>();
|
||||
resultOne.Value.Content.Should().NotBeNullOrEmpty();
|
||||
resultOne.Value.Usage.Should().Match<Usage>(u => u.CacheCreationInputTokens > 0 || u.CacheReadInputTokens > 0);
|
||||
|
||||
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
||||
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")]));
|
||||
|
||||
var resultTwo = await client.CreateMessageAsync(request);
|
||||
|
||||
resultTwo.IsSuccess.Should().BeTrue();
|
||||
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
||||
resultTwo.Value.Content.Should().NotBeNullOrEmpty();
|
||||
resultTwo.Value.Usage.CacheReadInputTokens.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenToolsContainCacheControl_ItShouldUseCache()
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "prompt-caching-2024-07-31");
|
||||
|
||||
var client = CreateClient(httpClient);
|
||||
|
||||
var func = (string ticker) => ticker;
|
||||
|
||||
var tools = Enumerable
|
||||
.Range(0, 50)
|
||||
.Select(i => Tool.CreateFromFunction($"tool-{i}", $"Tool {i}", func))
|
||||
.ToList();
|
||||
|
||||
tools.Last().CacheControl = new EphemeralCacheControl();
|
||||
|
||||
var request = new MessageRequest(
|
||||
model: AnthropicModels.Claude3Haiku,
|
||||
messages: [
|
||||
new(MessageRole.User, [
|
||||
new TextContent("Hi could you tell me your name?"),
|
||||
]),
|
||||
],
|
||||
tools: tools
|
||||
);
|
||||
|
||||
var resultOne = await client.CreateMessageAsync(request);
|
||||
|
||||
resultOne.IsSuccess.Should().BeTrue();
|
||||
resultOne.Value.Should().BeOfType<MessageResponse>();
|
||||
resultOne.Value.Content.Should().NotBeNullOrEmpty();
|
||||
resultOne.Value.Usage.Should().Match<Usage>(u => u.CacheCreationInputTokens > 0 || u.CacheReadInputTokens > 0);
|
||||
|
||||
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
||||
request.Messages.Add(new(MessageRole.User, [new TextContent("Could you tell me the stock price for AAPL?")]));
|
||||
|
||||
var resultTwo = await client.CreateMessageAsync(request);
|
||||
|
||||
resultTwo.IsSuccess.Should().BeTrue();
|
||||
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
||||
resultTwo.Value.Content.Should().NotBeNullOrEmpty();
|
||||
resultTwo.Value.Usage.CacheReadInputTokens.Should().BeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@ namespace AnthropicClient.Tests.EndToEnd;
|
||||
public class EndToEndTest(ConfigurationFixture configFixture) : IClassFixture<ConfigurationFixture>
|
||||
{
|
||||
protected readonly AnthropicApiClient _client = new(configFixture.AnthropicApiKey, new());
|
||||
protected AnthropicApiClient CreateClient(HttpClient httpClient) => new(configFixture.AnthropicApiKey, httpClient);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,69 @@
|
||||
The Forgotten Lighthouse
|
||||
|
||||
Sarah had always been drawn to the sea. As a child, she would spend hours on the beach, collecting shells and watching the waves crash against the shore. Now, at 28, she found herself living in a small coastal town, working as a marine biologist at the local research center. It was her dream job, allowing her to study the ocean and its inhabitants up close.
|
||||
|
||||
One stormy evening, as Sarah was walking along the beach after work, she noticed something peculiar in the distance. Through the mist and rain, she could make out the faint outline of a lighthouse she had never seen before. Intrigued, she decided to investigate.
|
||||
|
||||
As she approached the structure, Sarah realized why she had never noticed it before. The lighthouse was in a state of disrepair, its once-white paint now peeling and faded. Vines and moss clung to its sides, as if nature was slowly reclaiming it. The beacon at the top was dark, and Sarah wondered how long it had been since it had last guided ships to safety.
|
||||
|
||||
Despite the dilapidated appearance, there was something enchanting about the old lighthouse. Sarah felt drawn to it, as if it held secrets waiting to be discovered. She circled the base, looking for an entrance, and found a rusty door that creaked open with a gentle push.
|
||||
|
||||
Inside, the air was musty and thick with dust. Sarah pulled out her phone and turned on the flashlight, illuminating the circular room. Old furniture was scattered about, covered in sheets that had long since turned gray. In the center stood a spiral staircase, leading up to the top of the lighthouse.
|
||||
|
||||
Sarah hesitated for a moment, wondering if it was safe to climb the stairs in such an old building. But her curiosity got the better of her, and she began to ascend, each step groaning under her weight.
|
||||
|
||||
As she climbed, Sarah noticed old photographs hanging on the walls. They showed a family – a lighthouse keeper, his wife, and their young daughter – smiling in front of the once-pristine lighthouse. The images were faded and yellowed with age, but Sarah could still make out the happiness in their eyes.
|
||||
|
||||
When she reached the top, Sarah gasped. The view was breathtaking, even through the dirty windows. She could see for miles in every direction, the stormy sea stretching out to the horizon. The room was filled with old equipment – logbooks, maps, and a massive lens that once projected the lighthouse's beam across the water.
|
||||
|
||||
As Sarah explored the room, she noticed something odd. Despite the layer of dust covering everything, there was a small area on the desk that seemed clean, as if someone had recently been there. Next to it lay an old leather-bound journal.
|
||||
|
||||
Curious, Sarah picked up the journal and opened it. The pages were filled with neat handwriting, detailing the daily life of the lighthouse keeper. As she flipped through the pages, she realized that the entries spanned decades, far longer than one person's lifetime.
|
||||
|
||||
The last entry caught her eye. It was dated just a week ago:
|
||||
|
||||
"I've been here for so long, watching over the sea and guiding ships to safety. But times have changed, and my lighthouse is no longer needed. I fear I may soon fade away, just like the light I once tended. If anyone finds this journal, please remember us – the keepers of the light."
|
||||
|
||||
Sarah's hands trembled as she read the words. She looked around the room, half-expecting to see a ghost, but she was alone. As she turned back to the journal, a photograph slipped out from between the pages. It showed the same family she had seen in the pictures on the stairway, but this one was different. The image was crisp and clear, as if it had been taken recently, yet the people in it were dressed in old-fashioned clothes.
|
||||
|
||||
A chill ran down Sarah's spine. She quickly put the journal back on the desk and hurried down the stairs, her heart pounding. As she reached the bottom and stepped outside, she turned to look at the lighthouse one last time.
|
||||
|
||||
To her amazement, the lighthouse now appeared pristine and newly painted. The beacon at the top was shining brightly, cutting through the stormy night. Sarah rubbed her eyes, certain she must be seeing things, but when she looked again, the lighthouse was back to its dilapidated state.
|
||||
|
||||
Over the next few weeks, Sarah couldn't stop thinking about her experience at the lighthouse. She searched through town records and old newspapers, trying to find any information about the mysterious structure and its keepers. To her surprise, she found nothing. It was as if the lighthouse had never existed.
|
||||
|
||||
Determined to uncover the truth, Sarah returned to the lighthouse several times. Each visit left her with more questions than answers. Sometimes she would find fresh flowers on the desk upstairs, other times she would hear faint whispers or the sound of footsteps when she knew she was alone.
|
||||
|
||||
As months passed, Sarah became known in town as the woman obsessed with the old lighthouse. Some thought she was crazy, while others were intrigued by her tales. A few of the older residents even claimed to have seen the lighthouse shining on stormy nights, guiding ships to safety long after it had been abandoned.
|
||||
|
||||
Sarah's obsession began to affect her work and personal life. She spent less time at the research center and more time investigating the lighthouse's history. Her colleagues worried about her, but Sarah couldn't let go of the mystery.
|
||||
|
||||
One night, exactly a year after her first visit to the lighthouse, Sarah decided to spend the night there. She packed a sleeping bag, some food, and her camera, determined to capture any supernatural occurrences.
|
||||
|
||||
As she settled in for the night, the wind outside picked up, and rain began to lash against the windows. Sarah felt a mix of excitement and fear as she lay in her sleeping bag, watching the shadows dance on the walls.
|
||||
|
||||
Just as she was about to drift off to sleep, Sarah heard a sound that made her blood run cold. It was the clear, unmistakable sound of footsteps climbing the spiral staircase. She held her breath, her heart pounding in her chest, as the steps grew louder and closer.
|
||||
|
||||
The door to the room creaked open, and Sarah squeezed her eyes shut, too terrified to look. She felt a presence in the room, moving around her. Then, to her surprise, she heard a kind, elderly voice.
|
||||
|
||||
"Don't be afraid, my dear. We've been waiting for someone like you."
|
||||
|
||||
Sarah opened her eyes to find the room filled with a soft, warm light. Standing before her were the lighthouse keeper and his family from the photographs, smiling gently at her.
|
||||
|
||||
The keeper extended his hand to Sarah. "We've been looking for someone to take over our duties. Someone who loves the sea as much as we do. Will you join us and become the new keeper of the light?"
|
||||
|
||||
Sarah looked at the family, then out at the stormy sea beyond the windows. She thought about her life in town, her job at the research center, and the mystery that had consumed her for the past year. In that moment, she realized that she had never felt more at home than she did in this old lighthouse.
|
||||
|
||||
With a smile, Sarah took the keeper's hand and stood up. As she did, she felt a strange sensation, as if she were becoming part of the lighthouse itself. The years of decay melted away, and the beacon blazed to life, sending its light out across the turbulent waters.
|
||||
|
||||
From that night on, sailors would tell stories of the mysterious lighthouse that would appear on the darkest, stormiest nights, guiding them safely to shore. And if they looked closely, they might catch a glimpse of a young woman in the tower, keeping watch over the sea.
|
||||
|
||||
The town eventually forgot about Sarah, the marine biologist who had become obsessed with an old lighthouse. But on quiet nights, when the mist rolls in from the sea, some say they can still hear her laughter on the wind, eternally at peace in her new home by the sea.
|
||||
|
||||
Years passed, and the legend of the mysterious lighthouse grew. Sailors from all over the world shared tales of its miraculous appearances during treacherous storms. Some claimed it had saved them from certain doom, guiding them away from hidden reefs and dangerous shoals. Others spoke of catching glimpses of ghostly figures in the tower, tending to the light with unwavering dedication.
|
||||
|
||||
The small coastal town, once skeptical of Sarah's obsession, began to embrace the legend. Local artists painted scenes of the lighthouse, its beam cutting through stormy skies. Gift shops sold miniature replicas and postcards featuring artistic renderings of the structure. The town even started an annual festival called "The Keeper's Light," celebrating the mysterious lighthouse and its guardians.
|
||||
|
||||
As decades went by, the world changed. Modern navigation systems and GPS technology made traditional lighthouses obsolete. Many were decommissioned or turned into museums. But the forgotten lighthouse continued to appear when it was needed most, defying explanation and technology alike.
|
||||
|
||||
One day, a young girl named Emily, not unlike Sarah had been in her youth, stumbled upon an old journal in her grandmother's attic. As she read through the faded pages, she discovered the story of a marine biologist who had disappeared decades ago, leaving behind tales of a magical lighthouse. Intrigued, Emily felt a familiar pull towards the sea and the mysteries it held. And so, the cycle began anew, as another curious soul prepared to uncover the secrets of the forgotten lighthouse, ensuring that its light – and the memory of its keepers – would never truly fade away.
|
||||
@@ -13,7 +13,9 @@ public class AnthropicEventTests : SerializationTest
|
||||
""stop_sequence"": """",
|
||||
""usage"": {
|
||||
""input_tokens"": 472,
|
||||
""output_tokens"": 2
|
||||
""output_tokens"": 2,
|
||||
""cache_creation_input_tokens"": 0,
|
||||
""cache_read_input_tokens"": 0
|
||||
},
|
||||
""content"": [],
|
||||
""stop_reason"": """"
|
||||
@@ -70,7 +72,9 @@ public class AnthropicEventTests : SerializationTest
|
||||
},
|
||||
""usage"": {
|
||||
""output_tokens"": 89,
|
||||
""input_tokens"": 0
|
||||
""input_tokens"": 0,
|
||||
""cache_creation_input_tokens"": 0,
|
||||
""cache_read_input_tokens"": 0
|
||||
},
|
||||
""type"": ""message_delta""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace AnthropicClient.Tests.Unit.Models;
|
||||
|
||||
public class CacheControlTypeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Ephemeral_WhenCalled_ItShouldReturnExpectedValue()
|
||||
{
|
||||
CacheControlType.Ephemeral.Should().Be("ephemeral");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace AnthropicClient.Tests.Unit.Models;
|
||||
|
||||
public class EphemeralCacheControlTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WhenCalled_ItShouldInitializeType()
|
||||
{
|
||||
new EphemeralCacheControl().Type.Should().Be(CacheControlType.Ephemeral);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,16 @@ public class ImageContentTests : SerializationTest
|
||||
""type"": ""image""
|
||||
}";
|
||||
|
||||
private readonly string _testJsonWithCacheControl = @"{
|
||||
""source"": {
|
||||
""media_type"": ""image/png"",
|
||||
""data"": ""data"",
|
||||
""type"": ""base64""
|
||||
},
|
||||
""cache_control"": { ""type"": ""ephemeral"" },
|
||||
""type"": ""image""
|
||||
}";
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalled_ItShouldInitializeSource()
|
||||
{
|
||||
@@ -53,6 +63,41 @@ public class ImageContentTests : SerializationTest
|
||||
action.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithCacheControl_ItShouldInitializeSourceAndCacheControl()
|
||||
{
|
||||
var expectedMediaType = "image/png";
|
||||
var expectedData = "data";
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var result = new ImageContent(expectedMediaType, expectedData, cacheControl);
|
||||
|
||||
result.Source.Should().BeEquivalentTo(new ImageSource(expectedMediaType, expectedData));
|
||||
result.CacheControl.Should().BeSameAs(cacheControl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithCacheControlAndMediatTypeIsNull_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
var expectedData = "data";
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var action = () => new ImageContent(null!, expectedData, cacheControl);
|
||||
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithCacheControlAndDataIsNull_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
var expectedMediaType = "image/png";
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var action = () => new ImageContent(expectedMediaType, null!, cacheControl);
|
||||
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
|
||||
{
|
||||
@@ -63,6 +108,16 @@ public class ImageContentTests : SerializationTest
|
||||
JsonAssert.Equal(_testJson, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerializedWithCacheControl_ItShouldHaveExpectedShape()
|
||||
{
|
||||
var content = new ImageContent("image/png", "data", new EphemeralCacheControl());
|
||||
|
||||
var actual = Serialize(content);
|
||||
|
||||
JsonAssert.Equal(_testJsonWithCacheControl, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
|
||||
{
|
||||
|
||||
@@ -10,7 +10,9 @@ public class MessageDeltaEventDataTests : SerializationTest
|
||||
},
|
||||
""usage"": {
|
||||
""input_tokens"": 1,
|
||||
""output_tokens"": 1
|
||||
""output_tokens"": 1,
|
||||
""cache_creation_input_tokens"": 1,
|
||||
""cache_read_input_tokens"": 1
|
||||
}
|
||||
}";
|
||||
|
||||
@@ -18,7 +20,13 @@ public class MessageDeltaEventDataTests : SerializationTest
|
||||
public void Constructor_WhenCalled_ItShouldInitializeProperties()
|
||||
{
|
||||
var expectedDelta = new MessageDelta("max_tokens", "max_tokens");
|
||||
var expectedUsage = new Usage { InputTokens = 1, OutputTokens = 1 };
|
||||
var expectedUsage = new Usage
|
||||
{
|
||||
InputTokens = 1,
|
||||
OutputTokens = 1,
|
||||
CacheCreationInputTokens = 1,
|
||||
CacheReadInputTokens = 1,
|
||||
};
|
||||
|
||||
var messageDeltaEventData = new MessageDeltaEventData(expectedDelta, expectedUsage);
|
||||
|
||||
@@ -30,7 +38,13 @@ public class MessageDeltaEventDataTests : SerializationTest
|
||||
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
|
||||
{
|
||||
var expectedDelta = new MessageDelta("max_tokens", "max_tokens");
|
||||
var expectedUsage = new Usage { InputTokens = 1, OutputTokens = 1 };
|
||||
var expectedUsage = new Usage
|
||||
{
|
||||
InputTokens = 1,
|
||||
OutputTokens = 1,
|
||||
CacheCreationInputTokens = 1,
|
||||
CacheReadInputTokens = 1,
|
||||
};
|
||||
|
||||
var messageDeltaEventData = new MessageDeltaEventData(expectedDelta, expectedUsage);
|
||||
|
||||
@@ -43,7 +57,13 @@ public class MessageDeltaEventDataTests : SerializationTest
|
||||
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues()
|
||||
{
|
||||
var expectedDelta = new MessageDelta("max_tokens", "max_tokens");
|
||||
var expectedUsage = new Usage { InputTokens = 1, OutputTokens = 1 };
|
||||
var expectedUsage = new Usage
|
||||
{
|
||||
InputTokens = 1,
|
||||
OutputTokens = 1,
|
||||
CacheCreationInputTokens = 1,
|
||||
CacheReadInputTokens = 1,
|
||||
};
|
||||
|
||||
var messageDeltaEventData = Deserialize<MessageDeltaEventData>(_testJson);
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ public class MessageRequestTests : SerializationTest
|
||||
{
|
||||
private readonly string _testJson = @"{
|
||||
""model"": ""claude-3-sonnet-20240229"",
|
||||
""system"": ""test-system"",
|
||||
""system"": [{
|
||||
""type"": ""text"",
|
||||
""text"": ""test-system""
|
||||
}],
|
||||
""messages"": [
|
||||
{ ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"": ""text"" }] }
|
||||
],
|
||||
@@ -21,7 +24,10 @@ public class MessageRequestTests : SerializationTest
|
||||
|
||||
private readonly string _testJsonWithAnyToolChoice = @"{
|
||||
""model"": ""claude-3-sonnet-20240229"",
|
||||
""system"": ""test-system"",
|
||||
""system"": [{
|
||||
""type"": ""text"",
|
||||
""text"": ""test-system""
|
||||
}],
|
||||
""messages"":[
|
||||
{ ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"":""text"" }] }
|
||||
],
|
||||
@@ -38,7 +44,10 @@ public class MessageRequestTests : SerializationTest
|
||||
|
||||
private readonly string _testJsonWithSpecificToolChoice = @"{
|
||||
""model"": ""claude-3-sonnet-20240229"",
|
||||
""system"": ""test-system"",
|
||||
""system"": [{
|
||||
""type"": ""text"",
|
||||
""text"": ""test-system""
|
||||
}],
|
||||
""messages"": [
|
||||
{ ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"": ""text"" }] }
|
||||
],
|
||||
@@ -55,7 +64,10 @@ public class MessageRequestTests : SerializationTest
|
||||
|
||||
private readonly string _testJsonWithImageContent = @"{
|
||||
""model"": ""claude-3-sonnet-20240229"",
|
||||
""system"": ""test-system"",
|
||||
""system"": [{
|
||||
""type"": ""text"",
|
||||
""text"": ""test-system""
|
||||
}],
|
||||
""messages"":[
|
||||
{
|
||||
""role"": ""user"",
|
||||
@@ -80,7 +92,10 @@ public class MessageRequestTests : SerializationTest
|
||||
|
||||
private readonly string _testJsonWithUnknownContent = @"{
|
||||
""model"": ""claude-3-sonnet-20240229"",
|
||||
""system"": ""test-system"",
|
||||
""system"": [{
|
||||
""type"": ""text"",
|
||||
""text"": ""test-system""
|
||||
}],
|
||||
""messages"": [{ ""role"": ""user"", ""content"": [{ ""type"": ""unknown"", ""text"": ""text"" }] }],
|
||||
""max_tokens"": 512,
|
||||
""metadata"": { ""test"": ""test"" },
|
||||
@@ -95,7 +110,10 @@ public class MessageRequestTests : SerializationTest
|
||||
|
||||
private readonly string _testJsonWithToolUseContent = @"{
|
||||
""model"": ""claude-3-sonnet-20240229"",
|
||||
""system"": ""test-system"",
|
||||
""system"": [{
|
||||
""type"": ""text"",
|
||||
""text"": ""test-system""
|
||||
}],
|
||||
""messages"": [
|
||||
{
|
||||
""role"": ""assistant"",
|
||||
@@ -124,7 +142,10 @@ public class MessageRequestTests : SerializationTest
|
||||
|
||||
private readonly string _testJsonWithToolResultContent = @"{
|
||||
""model"": ""claude-3-sonnet-20240229"",
|
||||
""system"": ""test-system"",
|
||||
""system"": [{
|
||||
""type"": ""text"",
|
||||
""text"": ""test-system""
|
||||
}],
|
||||
""messages"": [
|
||||
{
|
||||
""role"": ""assistant"",
|
||||
@@ -301,13 +322,144 @@ public class MessageRequestTests : SerializationTest
|
||||
JsonAssert.Equal(_testJson, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerializedAndSystemMessagesAndSystemAreNull_ItShouldNotHaveSystemProperty()
|
||||
{
|
||||
var messageRequest = new MessageRequest(
|
||||
model: AnthropicModels.Claude3Haiku,
|
||||
messages: [
|
||||
new()
|
||||
{
|
||||
Role = MessageRole.User,
|
||||
Content = [new TextContent("Hello!")]
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
var expected = @"{
|
||||
""model"": ""claude-3-haiku-20240307"",
|
||||
""messages"": [
|
||||
{
|
||||
""role"": ""user"",
|
||||
""content"": [
|
||||
{
|
||||
""text"": ""Hello!"",
|
||||
""type"": ""text""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
""max_tokens"": 1024,
|
||||
""stop_sequences"": [],
|
||||
""temperature"": 0.0,
|
||||
""stream"": false
|
||||
}";
|
||||
|
||||
var actual = Serialize(messageRequest);
|
||||
|
||||
JsonAssert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerializedAndSystemMessagesAreProvided_ItShouldUseSystemMessagesForHaveSystemProperty()
|
||||
{
|
||||
var messageRequest = new MessageRequest(
|
||||
model: AnthropicModels.Claude3Haiku,
|
||||
messages: [
|
||||
new()
|
||||
{
|
||||
Role = MessageRole.User,
|
||||
Content = [new TextContent("Hello!")]
|
||||
}
|
||||
],
|
||||
systemMessages: [
|
||||
new TextContent("You are a helpful assistant.")
|
||||
],
|
||||
system: "test-system"
|
||||
);
|
||||
|
||||
var expected = @"{
|
||||
""model"": ""claude-3-haiku-20240307"",
|
||||
""system"": [
|
||||
{
|
||||
""text"": ""You are a helpful assistant."",
|
||||
""type"": ""text""
|
||||
}
|
||||
],
|
||||
""messages"": [
|
||||
{
|
||||
""role"": ""user"",
|
||||
""content"": [
|
||||
{
|
||||
""text"": ""Hello!"",
|
||||
""type"": ""text""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
""max_tokens"": 1024,
|
||||
""stop_sequences"": [],
|
||||
""temperature"": 0.0,
|
||||
""stream"": false
|
||||
}";
|
||||
|
||||
var actual = Serialize(messageRequest);
|
||||
|
||||
JsonAssert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerializedAndSystemMessageIsProvided_ItShouldUseSystemMessageForHaveSystemProperty()
|
||||
{
|
||||
var messageRequest = new MessageRequest(
|
||||
model: AnthropicModels.Claude3Haiku,
|
||||
messages: [
|
||||
new()
|
||||
{
|
||||
Role = MessageRole.User,
|
||||
Content = [new TextContent("Hello!")]
|
||||
}
|
||||
],
|
||||
system: "test-system"
|
||||
);
|
||||
|
||||
var expected = @"{
|
||||
""model"": ""claude-3-haiku-20240307"",
|
||||
""system"": [
|
||||
{
|
||||
""text"": ""test-system"",
|
||||
""type"": ""text""
|
||||
}
|
||||
],
|
||||
""messages"": [
|
||||
{
|
||||
""role"": ""user"",
|
||||
""content"": [
|
||||
{
|
||||
""text"": ""Hello!"",
|
||||
""type"": ""text""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
""max_tokens"": 1024,
|
||||
""stop_sequences"": [],
|
||||
""temperature"": 0.0,
|
||||
""stream"": false
|
||||
}";
|
||||
|
||||
var actual = Serialize(messageRequest);
|
||||
|
||||
JsonAssert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
|
||||
{
|
||||
var messageRequest = Deserialize<MessageRequest>(_testJson);
|
||||
|
||||
messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
|
||||
messageRequest.System.Should().Be("test-system");
|
||||
messageRequest.System.Should().BeNull();
|
||||
messageRequest.Messages.Should().HaveCount(1);
|
||||
messageRequest.MaxTokens.Should().Be(512);
|
||||
messageRequest.Metadata.Should().HaveCount(1);
|
||||
@@ -330,7 +482,7 @@ public class MessageRequestTests : SerializationTest
|
||||
var messageRequest = Deserialize<MessageRequest>(_testJsonWithAnyToolChoice);
|
||||
|
||||
messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
|
||||
messageRequest.System.Should().Be("test-system");
|
||||
messageRequest.System.Should().BeNull();
|
||||
messageRequest.Messages.Should().HaveCount(1);
|
||||
messageRequest.MaxTokens.Should().Be(512);
|
||||
messageRequest.Metadata.Should().HaveCount(1);
|
||||
@@ -352,7 +504,7 @@ public class MessageRequestTests : SerializationTest
|
||||
var messageRequest = Deserialize<MessageRequest>(_testJsonWithSpecificToolChoice);
|
||||
|
||||
messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
|
||||
messageRequest.System.Should().Be("test-system");
|
||||
messageRequest.System.Should().BeNull();
|
||||
messageRequest.Messages.Should().HaveCount(1);
|
||||
messageRequest.MaxTokens.Should().Be(512);
|
||||
messageRequest.Metadata.Should().HaveCount(1);
|
||||
@@ -387,7 +539,7 @@ public class MessageRequestTests : SerializationTest
|
||||
var messageRequest = Deserialize<MessageRequest>(_testJsonWithImageContent);
|
||||
|
||||
messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
|
||||
messageRequest.System.Should().Be("test-system");
|
||||
messageRequest.System.Should().BeNull();
|
||||
messageRequest.Messages.Should().HaveCount(1);
|
||||
messageRequest.MaxTokens.Should().Be(512);
|
||||
messageRequest.Metadata.Should().HaveCount(1);
|
||||
@@ -416,7 +568,7 @@ public class MessageRequestTests : SerializationTest
|
||||
var messageRequest = Deserialize<MessageRequest>(_testJsonWithToolUseContent);
|
||||
|
||||
messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
|
||||
messageRequest.System.Should().Be("test-system");
|
||||
messageRequest.System.Should().BeNull();
|
||||
messageRequest.Messages.Should().HaveCount(1);
|
||||
messageRequest.MaxTokens.Should().Be(512);
|
||||
messageRequest.Metadata.Should().HaveCount(1);
|
||||
@@ -447,7 +599,7 @@ public class MessageRequestTests : SerializationTest
|
||||
var messageRequest = Deserialize<MessageRequest>(_testJsonWithToolResultContent);
|
||||
|
||||
messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
|
||||
messageRequest.System.Should().Be("test-system");
|
||||
messageRequest.System.Should().BeNull();
|
||||
messageRequest.Messages.Should().HaveCount(1);
|
||||
messageRequest.MaxTokens.Should().Be(512);
|
||||
messageRequest.Metadata.Should().HaveCount(1);
|
||||
|
||||
@@ -53,7 +53,12 @@ public class MessageResponseTests : SerializationTest
|
||||
""stop_reason"": ""stop reason"",
|
||||
""stop_sequence"": ""stop sequence"",
|
||||
""type"": ""type"",
|
||||
""usage"": { ""input_tokens"": 1, ""output_tokens"": 2 },
|
||||
""usage"": {
|
||||
""input_tokens"": 1,
|
||||
""output_tokens"": 2,
|
||||
""cache_creation_input_tokens"": 0,
|
||||
""cache_read_input_tokens"": 0
|
||||
},
|
||||
""content"": [
|
||||
{ ""text"": ""text content"", ""type"": ""text"" }
|
||||
]
|
||||
@@ -93,7 +98,12 @@ public class MessageResponseTests : SerializationTest
|
||||
""stop_reason"": ""stop reason"",
|
||||
""stop_sequence"": ""stop sequence"",
|
||||
""type"": ""type"",
|
||||
""usage"": { ""input_tokens"": 1, ""output_tokens"": 2 },
|
||||
""usage"": {
|
||||
""input_tokens"": 1,
|
||||
""output_tokens"": 2,
|
||||
""cache_creation_input_tokens"": 0,
|
||||
""cache_read_input_tokens"": 0
|
||||
},
|
||||
""content"": [
|
||||
{ ""text"": ""text content"", ""type"": ""text"" }
|
||||
]
|
||||
|
||||
@@ -14,7 +14,9 @@ public class MessageStartEventDataTests : SerializationTest
|
||||
""stop_sequence"": """",
|
||||
""usage"": {
|
||||
""input_tokens"": 25,
|
||||
""output_tokens"": 1
|
||||
""output_tokens"": 1,
|
||||
""cache_creation_input_tokens"": 0,
|
||||
""cache_read_input_tokens"": 0
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
@@ -4,7 +4,10 @@ public class StreamMessageRequestTests : SerializationTest
|
||||
{
|
||||
private readonly string _testJson = @"{
|
||||
""model"": ""claude-3-sonnet-20240229"",
|
||||
""system"": ""test-system"",
|
||||
""system"": [{
|
||||
""type"": ""text"",
|
||||
""text"": ""test-system""
|
||||
}],
|
||||
""messages"": [
|
||||
{ ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"": ""text"" }] }
|
||||
],
|
||||
@@ -178,7 +181,7 @@ public class StreamMessageRequestTests : SerializationTest
|
||||
var messageRequest = Deserialize<StreamMessageRequest>(_testJson);
|
||||
|
||||
messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
|
||||
messageRequest.System.Should().Be("test-system");
|
||||
messageRequest.System.Should().BeNull();
|
||||
messageRequest.Messages.Should().HaveCount(1);
|
||||
messageRequest.MaxTokens.Should().Be(512);
|
||||
messageRequest.Metadata.Should().HaveCount(1);
|
||||
|
||||
@@ -3,6 +3,11 @@ namespace AnthropicClient.Tests.Unit.Models;
|
||||
public class TextContentTests : SerializationTest
|
||||
{
|
||||
private readonly string _testJson = @"{ ""text"": ""text"", ""type"": ""text"" }";
|
||||
private readonly string _testJsonWithCacheControl = @"{
|
||||
""text"": ""text"",
|
||||
""cache_control"": { ""type"": ""ephemeral"" },
|
||||
""type"": ""text""
|
||||
}";
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalled_ItShouldInitializeText()
|
||||
@@ -14,6 +19,18 @@ public class TextContentTests : SerializationTest
|
||||
result.Text.Should().Be(expectedText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithCacheControl_ItShouldInitializeProperties()
|
||||
{
|
||||
var expectedText = "text";
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var result = new TextContent(expectedText, cacheControl);
|
||||
|
||||
result.Text.Should().Be(expectedText);
|
||||
result.CacheControl.Should().BeSameAs(cacheControl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledAndTextIsNull_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
@@ -22,6 +39,16 @@ public class TextContentTests : SerializationTest
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithCacheControlAndTextIsNull_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var action = () => new TextContent(null!, cacheControl);
|
||||
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
|
||||
{
|
||||
@@ -32,6 +59,16 @@ public class TextContentTests : SerializationTest
|
||||
JsonAssert.Equal(_testJson, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerializedWithCacheControl_ItShouldHaveExpectedShape()
|
||||
{
|
||||
var content = new TextContent("text", new EphemeralCacheControl());
|
||||
|
||||
var actual = Serialize(content);
|
||||
|
||||
JsonAssert.Equal(_testJsonWithCacheControl, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
|
||||
{
|
||||
|
||||
@@ -35,6 +35,43 @@ public class ToolResultContentTests : SerializationTest
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledAndGivenCacheControl_ItShouldInitializeProperties()
|
||||
{
|
||||
var toolUseId = Guid.NewGuid().ToString();
|
||||
var content = "content";
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var actual = new ToolResultContent(toolUseId, content, cacheControl);
|
||||
|
||||
actual.ToolUseId.Should().Be(toolUseId);
|
||||
actual.Content.Should().Be(content);
|
||||
actual.CacheControl.Should().BeSameAs(cacheControl);
|
||||
actual.Type.Should().Be("tool_result");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledAndGivenCacheControlAndToolUseIdIsNull_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
var content = "content";
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var action = () => new ToolResultContent(null!, content, cacheControl);
|
||||
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledAndGivenCacheControlAndContentIsNull_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
var toolUseId = Guid.NewGuid().ToString();
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var action = () => new ToolResultContent(toolUseId, null!, cacheControl);
|
||||
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerialized_ItShouldReturnJsonString()
|
||||
{
|
||||
@@ -58,6 +95,34 @@ public class ToolResultContentTests : SerializationTest
|
||||
JsonAssert.Equal(expectedJson, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerializedWithCacheControl_ItShouldReturnJsonString()
|
||||
{
|
||||
var toolUseId = Guid.NewGuid().ToString();
|
||||
var content = "content";
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var expectedJson = @$"{{
|
||||
""tool_use_id"": ""{toolUseId}"",
|
||||
""content"": ""{content}"",
|
||||
""type"": ""tool_result"",
|
||||
""cache_control"": {{
|
||||
""type"": ""ephemeral""
|
||||
}}
|
||||
}}";
|
||||
|
||||
var toolResultContent = new ToolResultContent
|
||||
{
|
||||
ToolUseId = toolUseId,
|
||||
Content = content,
|
||||
CacheControl = cacheControl
|
||||
};
|
||||
|
||||
var actual = Serialize(toolResultContent);
|
||||
|
||||
JsonAssert.Equal(expectedJson, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonDeserialization_WhenDeserialized_ItShouldReturnToolResultContent()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace AnthropicClient.Tests.Unit.Models;
|
||||
|
||||
@@ -71,6 +70,17 @@ public class ToolTests : SerializationTest
|
||||
tool.Name.Should().HaveLength(64);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledAndGivenCacheControl_ItShouldInitializeCacheControl()
|
||||
{
|
||||
var method = () => true;
|
||||
var function = new AnthropicFunction(method.Method);
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
var tool = new Tool("name", "description", function, cacheControl);
|
||||
|
||||
tool.CacheControl.Should().BeSameAs(cacheControl);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(TestClass), null)]
|
||||
[InlineData(typeof(TestClass), "")]
|
||||
@@ -120,6 +130,29 @@ public class ToolTests : SerializationTest
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromStaticMethod_WhenCalledWithCacheControl_ItShouldReturnTool()
|
||||
{
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
var tool = Tool.CreateFromStaticMethod("test name", "description", typeof(TestClass), nameof(TestClass.TestStaticMethod), cacheControl);
|
||||
|
||||
var expectedSchema = new JsonObject()
|
||||
{
|
||||
["type"] = "object",
|
||||
};
|
||||
|
||||
tool.Name.Should().Be("test_name");
|
||||
tool.DisplayName.Should().Be("test name");
|
||||
tool.Description.Should().Be("description");
|
||||
tool.Function.Method.Name.Should().Be(nameof(TestClass.TestStaticMethod));
|
||||
tool.Function.Instance.Should().BeNull();
|
||||
tool.InputSchema.Should().BeEquivalentTo(
|
||||
expectedSchema,
|
||||
t => t.IgnoringCyclicReferences()
|
||||
);
|
||||
tool.CacheControl.Should().BeSameAs(cacheControl);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
@@ -178,6 +211,30 @@ public class ToolTests : SerializationTest
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromInstanceMethod_WhenCalledWithCacheControl_ItShouldReturnTool()
|
||||
{
|
||||
var instance = new TestClass();
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
var tool = Tool.CreateFromInstanceMethod("test name", "description", instance, nameof(instance.TestInstanceMethod), cacheControl);
|
||||
|
||||
var expectedSchema = new JsonObject()
|
||||
{
|
||||
["type"] = "object",
|
||||
};
|
||||
|
||||
tool.Name.Should().Be("test_name");
|
||||
tool.DisplayName.Should().Be("test name");
|
||||
tool.Description.Should().Be("description");
|
||||
tool.Function.Method.Name.Should().Be(nameof(TestClass.TestInstanceMethod));
|
||||
tool.Function.Instance.Should().Be(instance);
|
||||
tool.InputSchema.Should().BeEquivalentTo(
|
||||
expectedSchema,
|
||||
t => t.IgnoringCyclicReferences()
|
||||
);
|
||||
tool.CacheControl.Should().BeSameAs(cacheControl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromParameterlessFunction_WhenGivenNullFunction_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
@@ -207,6 +264,29 @@ public class ToolTests : SerializationTest
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromParameterlessFunction_WhenCalledWithCacheControl_ItShouldReturnTool()
|
||||
{
|
||||
var func = () => true;
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
var tool = Tool.CreateFromFunction("test name", "description", func, cacheControl);
|
||||
|
||||
var expectedSchema = new JsonObject()
|
||||
{
|
||||
["type"] = "object",
|
||||
};
|
||||
|
||||
tool.Name.Should().Be("test_name");
|
||||
tool.DisplayName.Should().Be("test name");
|
||||
tool.Description.Should().Be("description");
|
||||
tool.Function.Method.Should().BeSameAs(func.Method);
|
||||
tool.InputSchema.Should().BeEquivalentTo(
|
||||
expectedSchema,
|
||||
t => t.IgnoringCyclicReferences()
|
||||
);
|
||||
tool.CacheControl.Should().BeSameAs(cacheControl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromFunctionWithParameter_WhenGivenNullFunction_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
@@ -247,6 +327,40 @@ public class ToolTests : SerializationTest
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromFunctionWithParameter_WhenCalledWithCacheControl_ItShouldReturnTool()
|
||||
{
|
||||
var func = (string s) => true;
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
var tool = Tool.CreateFromFunction("test name", "description", func, cacheControl);
|
||||
|
||||
var expectedSchema = new JsonObject()
|
||||
{
|
||||
["type"] = "object",
|
||||
["properties"] = new JsonObject()
|
||||
{
|
||||
["s"] = new JsonObject()
|
||||
{
|
||||
["type"] = "string",
|
||||
},
|
||||
},
|
||||
["required"] = new JsonArray()
|
||||
{
|
||||
"s",
|
||||
},
|
||||
};
|
||||
|
||||
tool.Name.Should().Be("test_name");
|
||||
tool.DisplayName.Should().Be("test name");
|
||||
tool.Description.Should().Be("description");
|
||||
tool.Function.Method.Should().BeSameAs(func.Method);
|
||||
tool.InputSchema.Should().BeEquivalentTo(
|
||||
expectedSchema,
|
||||
t => t.IgnoringCyclicReferences()
|
||||
);
|
||||
tool.CacheControl.Should().BeSameAs(cacheControl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromClass_WhenCalledWithToolWhoseNameIsNull_ItShouldThrowException()
|
||||
{
|
||||
@@ -307,6 +421,29 @@ public class ToolTests : SerializationTest
|
||||
t => t.IgnoringCyclicReferences()
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromClass_WhenCalledWithProperToolAndCacheControl_ItShouldReturnTool()
|
||||
{
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
var tool = Tool.CreateFromClass<ProperTool>(cacheControl);
|
||||
|
||||
var expectedSchema = new JsonObject()
|
||||
{
|
||||
["type"] = "object",
|
||||
};
|
||||
|
||||
tool.Name.Should().Be("Name");
|
||||
tool.DisplayName.Should().Be("Name");
|
||||
tool.Description.Should().Be("Description");
|
||||
tool.Function.Method.Name.Should().Be(nameof(ProperTool.GetWeather));
|
||||
tool.Function.Instance.Should().BeOfType<ProperTool>();
|
||||
tool.InputSchema.Should().BeEquivalentTo(
|
||||
expectedSchema,
|
||||
t => t.IgnoringCyclicReferences()
|
||||
);
|
||||
tool.CacheControl.Should().BeSameAs(cacheControl);
|
||||
}
|
||||
}
|
||||
|
||||
class TestClass
|
||||
|
||||
@@ -22,6 +22,26 @@ public class ToolUseContentTests : SerializationTest
|
||||
actual.Type.Should().Be("tool_use");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CacheControl_WhenCalledToSetCacheControl_ItShouldSetCacheControl()
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
var name = "name";
|
||||
var input = new Dictionary<string, object?> { { "name", "input" } };
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var toolUseContent = new ToolUseContent()
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Input = input
|
||||
};
|
||||
|
||||
toolUseContent.CacheControl = cacheControl;
|
||||
|
||||
toolUseContent.CacheControl.Should().BeSameAs(cacheControl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerialized_ItShouldReturnJsonString()
|
||||
{
|
||||
@@ -48,6 +68,35 @@ public class ToolUseContentTests : SerializationTest
|
||||
JsonAssert.Equal(expectedJson, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerializedWithCacheControl_ItShouldReturnJsonString()
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
var name = "name";
|
||||
var input = new Dictionary<string, object?> { { "name", "input" } };
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var expectedJson = @$"{{
|
||||
""id"": ""{id}"",
|
||||
""name"": ""{name}"",
|
||||
""input"": {{ ""name"": ""input"" }},
|
||||
""cache_control"": {{ ""type"": ""ephemeral"" }},
|
||||
""type"": ""tool_use""
|
||||
}}";
|
||||
|
||||
var toolUseContent = new ToolUseContent()
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Input = input,
|
||||
CacheControl = cacheControl
|
||||
};
|
||||
|
||||
var actual = Serialize(toolUseContent);
|
||||
|
||||
JsonAssert.Equal(expectedJson, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonDeserialization_WhenDeserialized_ItShouldReturnToolUseContent()
|
||||
{
|
||||
|
||||
@@ -7,26 +7,39 @@ public class UsageTests : SerializationTest
|
||||
{
|
||||
var expectedInputTokens = 1;
|
||||
var expectedOutputTokens = 2;
|
||||
var expectedCacheCreationInputTokens = 3;
|
||||
var expectedCacheReadInputTokens = 4;
|
||||
|
||||
var usage = new Usage
|
||||
{
|
||||
InputTokens = expectedInputTokens,
|
||||
OutputTokens = expectedOutputTokens
|
||||
OutputTokens = expectedOutputTokens,
|
||||
CacheCreationInputTokens = expectedCacheCreationInputTokens,
|
||||
CacheReadInputTokens = expectedCacheReadInputTokens
|
||||
};
|
||||
|
||||
usage.InputTokens.Should().Be(expectedInputTokens);
|
||||
usage.OutputTokens.Should().Be(expectedOutputTokens);
|
||||
usage.CacheCreationInputTokens.Should().Be(expectedCacheCreationInputTokens);
|
||||
usage.CacheReadInputTokens.Should().Be(expectedCacheReadInputTokens);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenCalled_ItShouldSerializeCorrectly()
|
||||
{
|
||||
var expectedJson = @"{ ""input_tokens"": 1, ""output_tokens"": 2 }";
|
||||
var expectedJson = @"{
|
||||
""input_tokens"": 1,
|
||||
""output_tokens"": 2,
|
||||
""cache_creation_input_tokens"": 3,
|
||||
""cache_read_input_tokens"": 4
|
||||
}";
|
||||
|
||||
var usage = new Usage
|
||||
{
|
||||
InputTokens = 1,
|
||||
OutputTokens = 2
|
||||
OutputTokens = 2,
|
||||
CacheCreationInputTokens = 3,
|
||||
CacheReadInputTokens = 4
|
||||
};
|
||||
|
||||
var actual = Serialize(usage);
|
||||
@@ -37,11 +50,18 @@ public class UsageTests : SerializationTest
|
||||
[Fact]
|
||||
public void JsonDeserialization_WhenCalled_ItShouldDeserializeCorrectly()
|
||||
{
|
||||
var json = @"{ ""input_tokens"": 1, ""output_tokens"": 2 }";
|
||||
var json = @"{
|
||||
""input_tokens"": 1,
|
||||
""output_tokens"": 2,
|
||||
""cache_creation_input_tokens"": 3,
|
||||
""cache_read_input_tokens"": 4
|
||||
}";
|
||||
|
||||
var usage = Deserialize<Usage>(json);
|
||||
|
||||
usage!.InputTokens.Should().Be(1);
|
||||
usage.OutputTokens.Should().Be(2);
|
||||
usage.CacheCreationInputTokens.Should().Be(3);
|
||||
usage.CacheReadInputTokens.Should().Be(4);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user