feat: create chat message

This commit is contained in:
Stevan Freeborn
2024-06-27 23:26:53 -05:00
parent 68b359163d
commit 4c2cee643a
77 changed files with 3860 additions and 0 deletions
@@ -0,0 +1,29 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AnthropicClient.Models;
namespace AnthropicClient.Json;
class ContentConverter : JsonConverter<Content>
{
public override Content Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var jsonDocument = JsonDocument.ParseValue(ref reader);
var root = jsonDocument.RootElement;
var type = root.GetProperty("type").GetString();
return type switch
{
ContentType.Text => JsonSerializer.Deserialize<TextContent>(root.GetRawText(), options)!,
ContentType.Image => JsonSerializer.Deserialize<ImageContent>(root.GetRawText(), options)!,
ContentType.ToolUse => JsonSerializer.Deserialize<ToolUseContent>(root.GetRawText(), options)!,
ContentType.ToolResult => JsonSerializer.Deserialize<ToolResultContent>(root.GetRawText(), options)!,
_ => throw new JsonException($"Unknown content type: {type}")
};
}
public override void Write(Utf8JsonWriter writer, Content value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
@@ -0,0 +1,33 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AnthropicClient.Models;
namespace AnthropicClient.Json;
class ErrorConverter : JsonConverter<Error>
{
public override Error Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var jsonDocument = JsonDocument.ParseValue(ref reader);
var root = jsonDocument.RootElement;
var type = root.GetProperty("type").GetString();
return type switch
{
ErrorType.InvalidRequestError => JsonSerializer.Deserialize<InvalidRequestError>(root.GetRawText(), options)!,
ErrorType.AuthenticationError => JsonSerializer.Deserialize<AuthenticationError>(root.GetRawText(), options)!,
ErrorType.PermissionError => JsonSerializer.Deserialize<PermissionError>(root.GetRawText(), options)!,
ErrorType.NotFoundError => JsonSerializer.Deserialize<NotFoundError>(root.GetRawText(), options)!,
ErrorType.RateLimitError => JsonSerializer.Deserialize<RateLimitError>(root.GetRawText(), options)!,
ErrorType.ApiError => JsonSerializer.Deserialize<ApiError>(root.GetRawText(), options)!,
ErrorType.OverloadedError => JsonSerializer.Deserialize<OverloadedError>(root.GetRawText(), options)!,
_ => throw new JsonException($"Unknown error type: {type}")
};
}
public override void Write(Utf8JsonWriter writer, Error value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
@@ -0,0 +1,20 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AnthropicClient.Json;
static class JsonSerializationOptions
{
public static JsonSerializerOptions DefaultOptions => new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters =
{
new ContentConverter(),
new ToolChoiceConverter(),
new ErrorConverter(),
},
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
}
@@ -0,0 +1,28 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AnthropicClient.Models;
namespace AnthropicClient.Json;
class ToolChoiceConverter : JsonConverter<ToolChoice>
{
public override ToolChoice Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var jsonDocument = JsonDocument.ParseValue(ref reader);
var root = jsonDocument.RootElement;
var type = root.GetProperty("type").GetString();
return type switch
{
ToolChoiceType.Auto => JsonSerializer.Deserialize<AutoToolChoice>(root.GetRawText(), options)!,
ToolChoiceType.Any => JsonSerializer.Deserialize<AnyToolChoice>(root.GetRawText(), options)!,
ToolChoiceType.Tool => JsonSerializer.Deserialize<SpecificToolChoice>(root.GetRawText(), options)!,
_ => throw new JsonException($"Unknown tool choice type: {type}")
};
}
public override void Write(Utf8JsonWriter writer, ToolChoice value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
@@ -0,0 +1,34 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents an error response from the Anthropic API.
/// </summary>
public class AnthropicError
{
/// <summary>
/// The type of the error.
/// </summary>
public string Type { get; init; } = "error";
/// <summary>
/// The error object.
/// </summary>
public Error? Error { get; init; } = null;
[JsonConstructor]
internal AnthropicError()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AnthropicError"/> class.
/// </summary>
/// <param name="error">The error as an <see cref="Error"/> object.</param>
/// <returns>A new instance of the <see cref="AnthropicError"/> class.</returns>
public AnthropicError(Error error)
{
Error = error;
}
}
@@ -0,0 +1,29 @@
namespace AnthropicClient.Models;
/// <summary>
/// Provides constants for the Anthropic models.
/// </summary>
public static class AnthropicModels
{
/// <summary>
/// The Claude-3 Opus model.
/// </summary>
public const string Claude3Opus = "claude-3-opus-20240229";
/// <summary>
/// The Claude-3 Sonnet model.
/// </summary>
public const string Claude3Sonnet = "claude-3-sonnet-20240229";
/// <summary>
/// The Claude-3.5 Sonnet model.
/// </summary>
public const string Claude35Sonnet = "claude-3-5-sonnet-20240620";
/// <summary>
/// The Claude-3 Haiku model.
/// </summary>
public const string Claude3Haiku = "claude-3-haiku-20240307";
internal static bool IsValidModel(string modelId) => modelId is Claude3Opus or Claude3Sonnet or Claude35Sonnet or Claude3Haiku;
}
@@ -0,0 +1,66 @@
using AnthropicClient.Models;
/// <summary>
/// Represents the result of an Anthropic API operation.
/// </summary>
/// <typeparam name="T">The type of the result value.</typeparam>
public class AnthropicResult<T>
{
/// <summary>
/// The value of the result.
/// </summary>
public T Value { get; }
/// <summary>
/// The error of the result.
/// </summary>
public AnthropicError Error { get; }
/// <summary>
/// Indicates whether the operation was successful.
/// </summary>
public bool IsSuccess { get; }
/// <summary>
/// The request ID of the operation.
/// </summary>
public string RequestId { get; }
/// <summary>
/// Initializes a new instance of the <see cref="AnthropicResult{T}"/> class.
/// </summary>
/// <param name="value">The value of the result.</param>
/// <param name="error">The error of the result.</param>
/// <param name="isSuccess">Indicates whether the operation was successful.</param>
/// <param name="requestId">The request ID of the operation.</param>
/// <returns>A new instance of the <see cref="AnthropicResult{T}"/> class.</returns>
protected AnthropicResult(T value, AnthropicError error, bool isSuccess, string requestId)
{
Value = value;
Error = error;
IsSuccess = isSuccess;
RequestId = requestId;
}
/// <summary>
/// Creates a successful result.
/// </summary>
/// <param name="value">The value of the result.</param>
/// <param name="requestId">The request ID of the operation.</param>
/// <returns>A new instance of the <see cref="AnthropicResult{T}"/> class.</returns>
public static AnthropicResult<T> Success(T value, string requestId)
{
return new AnthropicResult<T>(value, null!, true, requestId);
}
/// <summary>
/// Creates a failed result.
/// </summary>
/// <param name="error">The error of the result.</param>
/// <param name="requestId">The request ID of the operation.</param>
/// <returns>A new instance of the <see cref="AnthropicResult{T}"/> class.</returns>
public static AnthropicResult<T> Failure(AnthropicError error, string requestId)
{
return new AnthropicResult<T>(default!, error, false, requestId);
}
}
@@ -0,0 +1,13 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the any tool choice mode.
/// </summary>
public class AnyToolChoice : ToolChoice
{
/// <summary>
/// Initializes a new instance of the <see cref="AnyToolChoice"/> class.
/// </summary>
/// <returns>A new instance of the <see cref="AnyToolChoice"/> class.</returns>
public AnyToolChoice() : base(ToolChoiceType.Any) { }
}
+24
View File
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents an api_error response from the Anthropic API.
/// </summary>
public class ApiError : Error
{
[JsonConstructor]
internal ApiError() : base(ErrorType.ApiError)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ApiError"/> class.
/// </summary>
/// <param name="message">The error message.</param>
/// <returns>A new instance of the <see cref="ApiError"/> class.</returns>
public ApiError(string message) : base(ErrorType.ApiError)
{
Message = message;
}
}
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents an authentication_error response from the Anthropic API.
/// </summary>
public class AuthenticationError : Error
{
[JsonConstructor]
internal AuthenticationError() : base(ErrorType.AuthenticationError)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationError"/> class.
/// </summary>
/// <param name="message">The error message.</param>
/// <returns>A new instance of the <see cref="AuthenticationError"/> class.</returns>
public AuthenticationError(string message) : base(ErrorType.AuthenticationError)
{
Message = message;
}
}
@@ -0,0 +1,13 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the auto tool choice mode.
/// </summary>
public class AutoToolChoice : ToolChoice
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoToolChoice"/> class.
/// </summary>
/// <returns>A new instance of the <see cref="AutoToolChoice"/> class.</returns>
public AutoToolChoice() : base(ToolChoiceType.Auto) { }
}
+48
View File
@@ -0,0 +1,48 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a chat message.
/// </summary>
public class ChatMessage
{
/// <summary>
/// Gets the role of the message.
/// </summary>
public string Role { get; init; } = string.Empty;
/// <summary>
/// Gets the contents of the message.
/// </summary>
public List<Content> Content { get; init; } = [];
[JsonConstructor]
internal ChatMessage()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChatMessage"/> class.
/// </summary>
/// <param name="role">The role of the message.</param>
/// <param name="content">The contents of the message.</param>
/// <exception cref="ArgumentException">Thrown when the role is invalid.</exception>
/// <exception cref="ArgumentNullException">Thrown when the role or content is null.</exception>
/// <returns>A new instance of the <see cref="ChatMessage"/> class.</returns>
public ChatMessage(string role, List<Content> content)
{
ArgumentValidator.ThrowIfNull(role, nameof(role));
ArgumentValidator.ThrowIfNull(content, nameof(content));
if (MessageRole.IsValidRole(role) is false)
{
throw new ArgumentException($"Invalid role: {role}");
}
Role = role;
Content = content;
}
}
@@ -0,0 +1,139 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a chat message request.
/// </summary>
public class ChatMessageRequest : MessageRequest
{
/// <summary>
/// Gets the model ID to use for the request.
/// </summary>
public string Model { get; init; } = string.Empty;
/// <summary>
/// Gets the system ID to use for the request.
/// </summary>
public string? System { get; init; } = null;
/// <summary>
/// Gets the messages to send to the model.
/// </summary>
public List<ChatMessage> Messages { get; init; } = [];
/// <summary>
/// Gets the maximum number of tokens to generate.
/// </summary>
[JsonPropertyName("max_tokens")]
public int MaxTokens { get; init; } = 1024;
/// <summary>
/// Gets the metadata to include with the request.
/// </summary>
public Dictionary<string, object>? Metadata { get; init; } = null;
/// <summary>
/// Gets the prompt stop sequences.
/// </summary>
[JsonPropertyName("stop_sequences")]
public List<string> StopSequences { get; init; } = [];
/// <summary>
/// Gets the temperature to use for the request.
/// </summary>
public decimal Temperature { get; init; } = 0.0m;
/// <summary>
/// Gets the top-K value to use for the request.
/// </summary>
public int? TopK { get; init; } = null;
/// <summary>
/// Gets the top-P value to use for the request.
/// </summary>
public decimal? TopP { get; init; } = null;
/// <summary>
/// Gets the tool choice mode to use for the request.
/// </summary>
[JsonPropertyName("tool_choice")]
public ToolChoice? ToolChoice { get; init; } = null;
/// <summary>
/// Gets the tools to use for the request.
/// </summary>
public List<Tool>? Tools { get; init; } = null;
[JsonConstructor]
internal ChatMessageRequest() : base(false) { }
/// <summary>
/// Initializes a new instance of the <see cref="ChatMessageRequest"/> class.
/// </summary>
/// <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="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>
/// <param name="topP">The top-P value to use for the request.</param>
/// <param name="toolChoice">The tool choice mode to use for the request.</param>
/// <param name="tools">The tools to use for the request.</param>
/// <exception cref="ArgumentException">Thrown when the model ID is invalid.</exception>
/// <exception cref="ArgumentNullException">Thrown when the model or messages is null.</exception>
/// <exception cref="ArgumentException">Thrown when the messages contain no messages.</exception>
/// <exception cref="ArgumentException">Thrown when the max tokens is less than one.</exception>
/// <exception cref="ArgumentException">Thrown when the temperature is less than zero or greater than one.</exception>
/// <returns>A new instance of the <see cref="ChatMessageRequest"/> class.</returns>
public ChatMessageRequest(
string model,
List<ChatMessage> 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
) : base(false)
{
ArgumentValidator.ThrowIfNull(model, nameof(model));
ArgumentValidator.ThrowIfNull(messages, nameof(messages));
if (AnthropicModels.IsValidModel(model) is false)
{
throw new ArgumentException($"Invalid model ID: {model}");
}
if (messages.Count < 1)
{
throw new ArgumentException("Messages must contain at least one message");
}
if (maxTokens < 1)
{
throw new ArgumentException($"Invalid max tokens: {maxTokens}");
}
if (temperature < 0.0m || temperature > 1.0m)
{
throw new ArgumentException($"Invalid temperature: {temperature}");
}
Model = model;
Messages = messages;
MaxTokens = maxTokens;
System = system;
Metadata = metadata;
Temperature = temperature;
TopK = topK;
TopP = topP;
ToolChoice = toolChoice;
Tools = tools;
}
}
@@ -0,0 +1,51 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a chat response.
/// </summary>
public class ChatResponse
{
/// <summary>
/// Gets the ID of the chat response.
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Gets the model used for the chat response.
/// </summary>
public string Model { get; set; } = string.Empty;
/// <summary>
/// Gets the role of the chat response.
/// </summary>
public string Role { get; set; } = string.Empty;
/// <summary>
/// Gets the stop reason of the chat response.
/// </summary>
[JsonPropertyName("stop_reason")]
public string StopReason { get; set; } = string.Empty;
/// <summary>
/// Gets the stop sequence of the chat response.
/// </summary>
[JsonPropertyName("stop_sequence")]
public string StopSequence { get; set; } = string.Empty;
/// <summary>
/// Gets the type of the chat response.
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// Gets the usage of the chat response.
/// </summary>
public ChatUsage Usage { get; set; } = new();
/// <summary>
/// Gets the contents of the chat response.
/// </summary>
public List<Content> Content { get; set; } = [];
}
+21
View File
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents the usage of a chat response.
/// </summary>
public class ChatUsage
{
/// <summary>
/// Gets the number of input tokens used.
/// </summary>
[JsonPropertyName("input_tokens")]
public int InputTokens { get; set; }
/// <summary>
/// Gets the number of output tokens used.
/// </summary>
[JsonPropertyName("output_tokens")]
public int OutputTokens { get; set; }
}
+83
View File
@@ -0,0 +1,83 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using AnthropicClient.Json;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a client for interacting with the Anthropic API.
/// </summary>
public interface IClient
{
/// <summary>
/// Creates a chat message asynchronously.
/// </summary>
/// <param name="request">The chat message request to create.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the chat response as an <see cref="AnthropicResult{T}"/>.</returns>
Task<AnthropicResult<ChatResponse>> CreateChatMessageAsync(ChatMessageRequest request);
}
/// <inheritdoc cref="IClient"/>
public class Client : IClient
{
private const string BaseUrl = "https://api.anthropic.com/v1/";
private const string ApiKeyHeader = "x-api-key";
private const string MessagesEndpoint = "messages";
private const string JsonContentType = "application/json";
private const string RequestIdHeader = "request-id";
private readonly Dictionary<string, string> _defaultHeaders = new()
{
{ "anthropic-version", "2023-06-01" },
};
private readonly HttpClient _httpClient;
/// <summary>
/// Initializes a new instance of the <see cref="Client"/> class.
/// </summary>
/// <param name="apiKey">The API key to use for the client.</param>
/// <param name="httpClient">The HTTP client to use for the client.</param>
/// <exception cref="ArgumentNullException">Thrown when the API key or HTTP client is null.</exception>
/// <returns>A new instance of the <see cref="Client"/> class.</returns>
public Client(string apiKey, HttpClient httpClient)
{
ArgumentValidator.ThrowIfNull(apiKey, nameof(apiKey));
ArgumentValidator.ThrowIfNull(httpClient, nameof(httpClient));
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri(BaseUrl);
_httpClient.DefaultRequestHeaders.Add(ApiKeyHeader, apiKey);
_httpClient.DefaultRequestHeaders
.Accept
.Add(new MediaTypeWithQualityHeaderValue(JsonContentType));
foreach (var pair in _defaultHeaders)
{
_httpClient.DefaultRequestHeaders.Add(pair.Key, pair.Value);
}
}
/// <inheritdoc />
public async Task<AnthropicResult<ChatResponse>> CreateChatMessageAsync(ChatMessageRequest request)
{
var requestContent = new StringContent(Serialize(request), Encoding.UTF8, JsonContentType);
var response = await _httpClient.PostAsync(MessagesEndpoint, requestContent);
var requestId = GetRequestId(response);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode is false)
{
var error = Deserialize<AnthropicError>(responseContent) ?? new AnthropicError();
return AnthropicResult<ChatResponse>.Failure(error, requestId);
}
var chatResponse = Deserialize<ChatResponse>(responseContent) ?? new ChatResponse();
return AnthropicResult<ChatResponse>.Success(chatResponse, requestId);
}
private string Serialize<T>(T obj) => JsonSerializer.Serialize(obj, JsonSerializationOptions.DefaultOptions);
private T? Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json, JsonSerializationOptions.DefaultOptions);
private string GetRequestId(HttpResponseMessage response) => response.Headers.GetValues(RequestIdHeader).FirstOrDefault() ?? string.Empty;
}
+29
View File
@@ -0,0 +1,29 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents part of the content of a chat message.
/// </summary>
public abstract class Content
{
/// <summary>
/// Gets the type of the content.
/// </summary>
public string Type { get; init; } = string.Empty;
[JsonConstructor]
internal Content()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Content"/> class.
/// </summary>
/// <param name="type">The type of the content.</param>
/// <returns>A new instance of the <see cref="Content"/> class.</returns>
protected Content(string type)
{
Type = type;
}
}
+27
View File
@@ -0,0 +1,27 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the content type.
/// </summary>
public static class ContentType
{
/// <summary>
/// Represents the text content type.
/// </summary>
public const string Text = "text";
/// <summary>
/// Represents the image content type.
/// </summary>
public const string Image = "image";
/// <summary>
/// Represents the tool use content type.
/// </summary>
public const string ToolUse = "tool_use";
/// <summary>
/// Represents the tool result content type.
/// </summary>
public const string ToolResult = "tool_result";
}
+27
View File
@@ -0,0 +1,27 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents an error.
/// </summary>
public abstract class Error
{
/// <summary>
/// Gets the type of the error.
/// </summary>
public string Type { get; init; } = string.Empty;
/// <summary>
/// Gets the message of the error.
/// </summary>
public string Message { get; init; } = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="Error"/> class.
/// </summary>
/// <param name="type">The type of the error.</param>
/// <returns>A new instance of the <see cref="Error"/> class.</returns>
protected Error(string type)
{
Type = type;
}
}
+42
View File
@@ -0,0 +1,42 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the error type.
/// </summary>
public class ErrorType
{
/// <summary>
/// Represents the invalid_request error type.
/// </summary>
public const string InvalidRequestError = "invalid_request_error";
/// <summary>
/// Represents the authentication_error type.
/// </summary>
public const string AuthenticationError = "authentication_error";
/// <summary>
/// Represents the permission_error type.
/// </summary>
public const string PermissionError = "permission_error";
/// <summary>
/// Represents the not_found_error type.
/// </summary>
public const string NotFoundError = "not_found_error";
/// <summary>
/// Represents the rate_limit_error type.
/// </summary>
public const string RateLimitError = "rate_limit_error";
/// <summary>
/// Represents the api_error type.
/// </summary>
public const string ApiError = "api_error";
/// <summary>
/// Represents the overloaded_error type.
/// </summary>
public const string OverloadedError = "overloaded_error";
}
@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents image content that is part of a chat message.
/// </summary>
public class ImageContent : Content
{
/// <summary>
/// Gets the source of the image.
/// </summary>
public ImageSource Source { get; init; } = new();
[JsonConstructor]
internal ImageContent()
{
}
/// <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>
/// <exception cref="ArgumentNullException">Thrown when the media type or data is null.</exception>
/// <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));
Source = new(mediaType, data);
}
}
+50
View File
@@ -0,0 +1,50 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents an image source.
/// </summary>
public class ImageSource
{
/// <summary>
/// Gets the media type of the image.
/// </summary>
[JsonPropertyName("media_type")]
public string MediaType { get; init; } = string.Empty;
/// <summary>
/// Gets the data of the image.
/// </summary>
public string Data { get; init; } = string.Empty;
[JsonConstructor]
internal ImageSource()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ImageSource"/> class.
/// </summary>
/// <param name="mediaType">The media type of the image.</param>
/// <param name="data">The data of the image.</param>
/// <exception cref="ArgumentException">Thrown when the media type is invalid.</exception>
/// <exception cref="ArgumentNullException">Thrown when the media type or data is null.</exception>
/// <returns>A new instance of the <see cref="ImageSource"/> class.</returns>
public ImageSource(string mediaType, string data)
{
ArgumentValidator.ThrowIfNull(mediaType, nameof(mediaType));
if (ImageType.IsValidImageType(mediaType) is false)
{
throw new ArgumentException($"Invalid media type: {mediaType}");
}
ArgumentValidator.ThrowIfNull(data, nameof(data));
MediaType = mediaType;
Data = data;
}
}
+29
View File
@@ -0,0 +1,29 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the image type.
/// </summary>
public static class ImageType
{
/// <summary>
/// Represents the JPEG image type.
/// </summary>
public const string Jpg = "image/jpeg";
/// <summary>
/// Represents the PNG image type.
/// </summary>
public const string Png = "image/png";
/// <summary>
/// Represents the GIF image type.
/// </summary>
public const string Gif = "image/gif";
/// <summary>
/// Represents the WebP image type.
/// </summary>
public const string Webp = "image/webp";
internal static bool IsValidImageType(string imageType) => imageType is Jpg or Png or Gif or Webp;
}
@@ -0,0 +1,42 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents an input property.
/// </summary>
public class InputProperty
{
/// <summary>
/// Gets the type of the input property.
/// </summary>
public string Type { get; init; } = string.Empty;
/// <summary>
/// Gets the description of the input property.
/// </summary>
public string Description { get; init; } = string.Empty;
[JsonConstructor]
internal InputProperty()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InputProperty"/> class.
/// </summary>
/// <param name="type">The type of the input property.</param>
/// <param name="description">The description of the input property.</param>
/// <exception cref="ArgumentNullException">Thrown when the type or description is null.</exception>
/// <returns>A new instance of the <see cref="InputProperty"/> class.</returns>
public InputProperty(string type, string description)
{
ArgumentValidator.ThrowIfNull(type, nameof(type));
ArgumentValidator.ThrowIfNull(description, nameof(description));
Type = type;
Description = description;
}
}
+53
View File
@@ -0,0 +1,53 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents an input schema.
/// </summary>
public class InputSchema
{
/// <summary>
/// Gets the type of the input schema.
/// </summary>
public string Type { get; init; } = "object";
/// <summary>
/// Gets the properties of the input schema.
/// </summary>
public Dictionary<string, InputProperty> Properties { get; init; } = [];
/// <summary>
/// Gets the required properties of the input schema.
/// </summary>
public List<string> Required { get; init; } = [];
[JsonConstructor]
internal InputSchema()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InputSchema"/> class.
/// </summary>
/// <param name="properties">The properties of the input schema.</param>
/// <param name="required">The required properties of the input schema.</param>
/// <exception cref="ArgumentNullException">Thrown when the properties or required is null.</exception>
/// <exception cref="ArgumentException">Thrown when the required properties are not present in the properties dictionary.</exception>
/// <returns>A new instance of the <see cref="InputSchema"/> class.</returns>
public InputSchema(Dictionary<string, InputProperty> properties, List<string> required)
{
ArgumentValidator.ThrowIfNull(properties, nameof(properties));
ArgumentValidator.ThrowIfNull(required, nameof(required));
if (required.Any(r => properties.ContainsKey(r) is false))
{
throw new ArgumentException("Required properties must be present in the properties dictionary.");
}
Properties = properties;
Required = required;
}
}
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents an invalid_request error.
/// </summary>
public class InvalidRequestError : Error
{
[JsonConstructor]
internal InvalidRequestError() : base(ErrorType.InvalidRequestError)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InvalidRequestError"/> class.
/// </summary>
/// <param name="message">The message of the error.</param>
/// <returns>A new instance of the <see cref="InvalidRequestError"/> class.</returns>
public InvalidRequestError(string message) : base(ErrorType.InvalidRequestError)
{
Message = message;
}
}
@@ -0,0 +1,7 @@
using System.ComponentModel;
namespace System.Runtime.CompilerServices
{
[EditorBrowsable(EditorBrowsableState.Never)]
class IsExternalInit { }
}
@@ -0,0 +1,22 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents a message request.
/// </summary>
public abstract class MessageRequest
{
/// <summary>
/// Gets a value indicating whether the message should be streamed.
/// </summary>
public bool Stream { get; init; }
/// <summary>
/// Initializes a new instance of the <see cref="MessageRequest"/> class.
/// </summary>
/// <param name="stream">A value indicating whether the message should be streamed.</param>
/// <returns>A new instance of the <see cref="MessageRequest"/> class.</returns>
public MessageRequest(bool stream = false)
{
Stream = stream;
}
}
+19
View File
@@ -0,0 +1,19 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the message role.
/// </summary>
public static class MessageRole
{
/// <summary>
/// Represents the user role.
/// </summary>
public const string User = "user";
/// <summary>
/// Represents the assistant role.
/// </summary>
public const string Assistant = "assistant";
internal static bool IsValidRole(string role) => role is User or Assistant;
}
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a not_found error.
/// </summary>
public class NotFoundError : Error
{
[JsonConstructor]
internal NotFoundError() : base(ErrorType.NotFoundError)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="NotFoundError"/> class.
/// </summary>
/// <param name="message">The message of the error.</param>
/// <returns>A new instance of the <see cref="NotFoundError"/> class.</returns>
public NotFoundError(string message) : base(ErrorType.NotFoundError)
{
Message = message;
}
}
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents an overloaded error.
/// </summary>
public class OverloadedError : Error
{
[JsonConstructor]
internal OverloadedError() : base(ErrorType.OverloadedError)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OverloadedError"/> class.
/// </summary>
/// <param name="message">The message of the error.</param>
/// <returns>A new instance of the <see cref="OverloadedError"/> class.</returns>
public OverloadedError(string message) : base(ErrorType.OverloadedError)
{
Message = message;
}
}
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a permission error.
/// </summary>
public class PermissionError : Error
{
[JsonConstructor]
internal PermissionError() : base(ErrorType.PermissionError)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PermissionError"/> class.
/// </summary>
/// <param name="message">The message of the error.</param>
/// <returns>A new instance of the <see cref="PermissionError"/> class.</returns>
public PermissionError(string message) : base(ErrorType.PermissionError)
{
Message = message;
}
}
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a rate_limit error.
/// </summary>
public class RateLimitError : Error
{
[JsonConstructor]
internal RateLimitError() : base(ErrorType.RateLimitError)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RateLimitError"/> class.
/// </summary>
/// <param name="message">The message of the error.</param>
/// <returns>A new instance of the <see cref="RateLimitError"/> class.</returns>
public RateLimitError(string message) : base(ErrorType.RateLimitError)
{
Message = message;
}
}
@@ -0,0 +1,34 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents the specific tool choice mode.
/// </summary>
public class SpecificToolChoice : ToolChoice
{
/// <summary>
/// Gets the name of the tool.
/// </summary>
public string Name { get; init; } = string.Empty;
[JsonConstructor]
internal SpecificToolChoice() : base(ToolChoiceType.Tool)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SpecificToolChoice"/> class.
/// </summary>
/// <param name="name">The name of the tool.</param>
/// <exception cref="ArgumentNullException">Thrown when the name is null.</exception>
/// <returns>A new instance of the <see cref="SpecificToolChoice"/> class.</returns>
public SpecificToolChoice(string name) : base(ToolChoiceType.Tool)
{
ArgumentValidator.ThrowIfNull(name, nameof(name));
Name = name;
}
}
@@ -0,0 +1,27 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the stop reason type.
/// </summary>
public static class StopReasonType
{
/// <summary>
/// Represents the end_turn stop reason.
/// </summary>
public const string EndTurn = "end_turn";
/// <summary>
/// Represents the max_tokens stop reason.
/// </summary>
public const string MaxTokens = "max_tokens";
/// <summary>
/// Represents the stop_sequence stop reason.
/// </summary>
public const string StopSequence = "stop_sequence";
/// <summary>
/// Represents the tool_use stop reason.
/// </summary>
public const string ToolUse = "tool_use";
}
+34
View File
@@ -0,0 +1,34 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents text content that is part of a chat message.
/// </summary>
public class TextContent : Content
{
/// <summary>
/// Gets the text of the content.
/// </summary>
public string Text { get; init; } = string.Empty;
[JsonConstructor]
internal TextContent() : base(ContentType.Text)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="TextContent"/> class.
/// </summary>
/// <param name="text">The text of the content.</param>
/// <exception cref="ArgumentNullException">Thrown when the text is null.</exception>
/// <returns>A new instance of the <see cref="TextContent"/> class.</returns>
public TextContent(string text) : base(ContentType.Text)
{
ArgumentValidator.ThrowIfNull(text, nameof(text));
Text = text;
}
}
+51
View File
@@ -0,0 +1,51 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a tool that can be used in the chat.
/// </summary>
public class Tool
{
/// <summary>
/// Gets the name of the tool.
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// Gets the description of the tool.
/// </summary>
public string Description { get; init; } = string.Empty;
/// <summary>
/// Gets the input schema of the tool.
/// </summary>
[JsonPropertyName("input_schema")]
public InputSchema InputSchema { get; init; } = new();
[JsonConstructor]
internal Tool()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Tool"/> class.
/// </summary>
/// <param name="name">The name of the tool.</param>
/// <param name="description">The description of the tool.</param>
/// <param name="inputSchema">The input schema of the tool.</param>
/// <exception cref="ArgumentNullException">Thrown when the name, description, or input schema is null.</exception>
/// <returns>A new instance of the <see cref="Tool"/> class.</returns>
public Tool(string name, string description, InputSchema inputSchema)
{
ArgumentValidator.ThrowIfNull(name, nameof(name));
ArgumentValidator.ThrowIfNull(description, nameof(description));
ArgumentValidator.ThrowIfNull(inputSchema, nameof(inputSchema));
Name = name;
Description = description;
InputSchema = inputSchema;
}
}
+22
View File
@@ -0,0 +1,22 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents a tool choice mode.
/// </summary>
public abstract class ToolChoice
{
/// <summary>
/// Gets the type of the tool choice.
/// </summary>
public string Type { get; init; } = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="ToolChoice"/> class.
/// </summary>
/// <param name="type">The type of the tool choice.</param>
/// <returns>A new instance of the <see cref="ToolChoice"/> class.</returns>
protected ToolChoice(string type)
{
Type = type;
}
}
@@ -0,0 +1,24 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the tool choice type.
/// </summary>
public class ToolChoiceType
{
/// <summary>
/// Represents the auto tool choice type.
/// </summary>
public const string Auto = "auto";
/// <summary>
/// Represents the any tool choice type.
/// </summary>
public const string Any = "any";
/// <summary>
/// Represents the specific tool choice type.
/// </summary>
public const string Tool = "tool";
internal static bool IsValidType(string type) => type is Auto or Any or Tool;
}
@@ -0,0 +1,41 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents tool result content that is part of a chat message.
/// </summary>
public class ToolResultContent : Content
{
/// <summary>
/// Gets the tool use ID of the content.
/// </summary>
[JsonPropertyName("tool_use_id")]
public string ToolUseId { get; set; } = string.Empty;
/// <summary>
/// Gets the content of the tool result.
/// </summary>
public string Content { get; set; } = string.Empty;
[JsonConstructor]
internal ToolResultContent() : base(ContentType.ToolResult) { }
/// <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>
/// <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) : base(ContentType.ToolResult)
{
ArgumentValidator.ThrowIfNull(toolUseId, nameof(toolUseId));
ArgumentValidator.ThrowIfNull(content, nameof(content));
ToolUseId = toolUseId;
Content = content;
}
}
@@ -0,0 +1,30 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents tool use content that is part of a message.
/// </summary>
public class ToolUseContent : Content
{
/// <summary>
/// Gets the ID of the tool use.
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Gets the name of the tool.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets the input of the tool.
/// </summary>
public Dictionary<string, object?> Input { get; set; } = [];
/// <summary>
/// Initializes a new instance of the <see cref="ToolUseContent"/> class.
/// </summary>
/// <returns>A new instance of the <see cref="ToolUseContent"/> class.</returns>
public ToolUseContent() : base(ContentType.ToolUse)
{
}
}
@@ -0,0 +1,23 @@
namespace AnthropicClient.Utils;
/// <summary>
/// Provides methods to validate arguments.
/// </summary>
public static class ArgumentValidator
{
/// <summary>
/// Throws an <see cref="ArgumentNullException"/> if the value is null.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="value">The value to check.</param>
/// <param name="name">The name of the value.</param>
/// <exception cref="ArgumentNullException">Thrown when the value is null.</exception>
/// <returns>Nothing.</returns>
public static void ThrowIfNull<T>(T? value, string name)
{
if (value is null)
{
throw new ArgumentNullException(name);
}
}
}
@@ -0,0 +1,21 @@
namespace AnthropicClient.Tests.EndToEnd;
public class ClientTests(
HttpClientFixture httpClientFixture,
ConfigurationFixture configFixture
) : EndToEndTest(httpClientFixture, configFixture)
{
[Fact]
public async Task CreateChatMessage_WhenCalled_ShouldReturnChatResponse()
{
var request = new ChatMessageRequest(
model: AnthropicModels.Claude3Haiku,
messages: [new(MessageRole.User, [new TextContent("Hello!")])]
);
var result = await _client.CreateChatMessageAsync(request);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<ChatResponse>();
}
}
@@ -0,0 +1,23 @@
namespace AnthropicClient.Tests.EndToEnd;
public class ConfigurationFixture
{
public string AnthropicApiKey { get; }
public ConfigurationFixture()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.Test.json")
.AddEnvironmentVariables()
.Build();
var anthropicApiKey = configuration["AnthropicApiKey"];
if (string.IsNullOrEmpty(anthropicApiKey))
{
throw new InvalidOperationException("AnthropicApiKey is required");
}
AnthropicApiKey = anthropicApiKey;
}
}
@@ -0,0 +1,9 @@
namespace AnthropicClient.Tests.EndToEnd;
public class EndToEndTest(
HttpClientFixture httpClientFixture,
ConfigurationFixture configFixture
) : IClassFixture<HttpClientFixture>, IClassFixture<ConfigurationFixture>
{
protected readonly Client _client = new(configFixture.AnthropicApiKey, httpClientFixture.HttpClient);
}
@@ -0,0 +1,11 @@
namespace AnthropicClient.Tests.EndToEnd;
public class HttpClientFixture
{
public HttpClient HttpClient { get; }
public HttpClientFixture()
{
HttpClient = new HttpClient();
}
}
@@ -0,0 +1,168 @@
namespace AnthropicClient.Tests.Unit.Models;
public class AnthropicErrorTests : SerializationTest
{
private readonly string _testApiErrorJson = @"{
""type"": ""error"",
""error"": {
""type"": ""api_error"",
""message"": ""message""
}
}";
private readonly string _testAuthenticationErrorJson = @"{
""type"": ""error"",
""error"": {
""type"": ""authentication_error"",
""message"": ""message""
}
}";
private readonly string _testRateLimitErrorJson = @"{
""type"": ""error"",
""error"": {
""type"": ""rate_limit_error"",
""message"": ""message""
}
}";
private readonly string _testPermissionErrorJson = @"{
""type"": ""error"",
""error"": {
""type"": ""permission_error"",
""message"": ""message""
}
}";
private readonly string _testNotFoundErrorJson = @"{
""type"": ""error"",
""error"": {
""type"": ""not_found_error"",
""message"": ""message""
}
}";
private readonly string _testOverloadedErrorJson = @"{
""type"": ""error"",
""error"": {
""type"": ""overloaded_error"",
""message"": ""message""
}
}";
private readonly string _testInvalidRequestErrorJson = @"{
""type"": ""error"",
""error"": {
""type"": ""invalid_request_error"",
""message"": ""message""
}
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var apiError = new ApiError("message");
var error = new AnthropicError(apiError);
error.Type.Should().Be("error");
error.Error.Should().BeSameAs(apiError);
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldBeExpectedShape()
{
var apiError = new ApiError("message");
var error = new AnthropicError(apiError);
var json = Serialize(error);
JsonAssert.Equal(_testApiErrorJson, json);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldBeExpectedModel()
{
var error = Deserialize<AnthropicError>(_testApiErrorJson);
error!.Type.Should().Be("error");
error.Error.Should().BeOfType<ApiError>();
error.Error!.Message.Should().Be("message");
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldBeExpectedModelForAuthenticationError()
{
var error = Deserialize<AnthropicError>(_testAuthenticationErrorJson);
error!.Type.Should().Be("error");
error.Error.Should().BeOfType<AuthenticationError>();
error.Error!.Message.Should().Be("message");
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldBeExpectedModelForRateLimitError()
{
var error = Deserialize<AnthropicError>(_testRateLimitErrorJson);
error!.Type.Should().Be("error");
error.Error.Should().BeOfType<RateLimitError>();
error.Error!.Message.Should().Be("message");
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldBeExpectedModelForPermissionError()
{
var error = Deserialize<AnthropicError>(_testPermissionErrorJson);
error!.Type.Should().Be("error");
error.Error.Should().BeOfType<PermissionError>();
error.Error!.Message.Should().Be("message");
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldBeExpectedModelForNotFoundError()
{
var error = Deserialize<AnthropicError>(_testNotFoundErrorJson);
error!.Type.Should().Be("error");
error.Error.Should().BeOfType<NotFoundError>();
error.Error!.Message.Should().Be("message");
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldBeExpectedModelForOverloadedError()
{
var error = Deserialize<AnthropicError>(_testOverloadedErrorJson);
error!.Type.Should().Be("error");
error.Error.Should().BeOfType<OverloadedError>();
error.Error!.Message.Should().Be("message");
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldBeExpectedModelForInvalidRequestError()
{
var error = Deserialize<AnthropicError>(_testInvalidRequestErrorJson);
error!.Type.Should().Be("error");
error.Error.Should().BeOfType<InvalidRequestError>();
error.Error!.Message.Should().Be("message");
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldBeExpectedModelForUnknownError()
{
var json = @"{
""type"": ""error"",
""error"": {
""type"": ""unknown_error"",
""message"": ""message""
}
}";
var action = () => Deserialize<AnthropicError>(json);
action.Should().Throw<JsonException>();
}
}
@@ -0,0 +1,67 @@
namespace AnthropicClient.Tests.Unit.Models;
public class AnthropicModelsTests
{
[Fact]
public void Claude3Opus_WhenCalled_ItShouldReturnExpectedValue()
{
var expected = "claude-3-opus-20240229";
var actual = AnthropicModels.Claude3Opus;
actual.Should().Be(expected);
}
[Fact]
public void Claude3Sonnet_WhenCalled_ItShouldReturnExpectedValue()
{
var expected = "claude-3-sonnet-20240229";
var actual = AnthropicModels.Claude3Sonnet;
actual.Should().Be(expected);
}
[Fact]
public void Claude35Sonnet_WhenCalled_ItShouldReturnExpectedValue()
{
var expected = "claude-3-5-sonnet-20240620";
var actual = AnthropicModels.Claude35Sonnet;
actual.Should().Be(expected);
}
[Fact]
public void Claude3Haiku_WhenCalled_ItShouldReturnExpectedValue()
{
var expected = "claude-3-haiku-20240307";
var actual = AnthropicModels.Claude3Haiku;
actual.Should().Be(expected);
}
[Fact]
public void Claude35Sonnet_WhenCalled_ItShouldExpectedValue()
{
var expected = "claude-3-5-sonnet-20240620";
var actual = AnthropicModels.Claude35Sonnet;
actual.Should().Be(expected);
}
[Theory]
[InlineData("claude-3-opus-20240229", true)]
[InlineData("claude-3-sonnet-20240229", true)]
[InlineData("claude-3-5-sonnet-20240620", true)]
[InlineData("claude-3-haiku-20240307", true)]
[InlineData("invalid", false)]
public void IsValidModel_WhenCalled_ItShouldReturnExpectedValue(string modelId, bool expected)
{
var actual = AnthropicModels.IsValidModel(modelId);
actual.Should().Be(expected);
}
}
@@ -0,0 +1,30 @@
namespace AnthropicClient.Tests.Unit.Models;
public class AnthropicResultTests
{
[Fact]
public void Success_WhenCalled_ItShouldReturnSuccessResult()
{
var value = "success";
var requestId = Guid.NewGuid().ToString();
var actual = AnthropicResult<string>.Success(value, requestId);
actual.IsSuccess.Should().BeTrue();
actual.Value.Should().Be(value);
actual.RequestId.Should().Be(requestId);
}
[Fact]
public void Failure_WhenCalled_ItShouldReturnFailureResult()
{
var error = new AnthropicError(new AuthenticationError("message"));
var requestId = Guid.NewGuid().ToString();
var actual = AnthropicResult<string>.Failure(error, requestId);
actual.IsSuccess.Should().BeFalse();
actual.Error.Should().Be(error);
actual.RequestId.Should().Be(requestId);
}
}
@@ -0,0 +1,38 @@
namespace AnthropicClient.Tests.Unit.Models;
public class AnyToolChoiceTests : SerializationTest
{
private readonly string _testJson = @"{
""type"": ""any""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldSetTypeToAny()
{
var expected = "any";
var actual = new AnyToolChoice();
actual.Type.Should().Be(expected);
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var choice = new AnyToolChoice();
var actual = Serialize(choice);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var expected = new AnyToolChoice();
var actual = Deserialize<AnyToolChoice>(_testJson);
actual.Should().BeEquivalentTo(expected);
}
}
@@ -0,0 +1,40 @@
namespace AnthropicClient.Tests.Unit.Models;
public class AuthenticationErrorTests : SerializationTest
{
private readonly string _testJson = @"{
""message"": ""message"",
""type"": ""authentication_error""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var expectedMessage = "message";
var actual = new AuthenticationError(expectedMessage);
actual.Message.Should().Be(expectedMessage);
actual.Type.Should().Be("authentication_error");
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var error = new AuthenticationError("message");
var actual = Serialize(error);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var expected = new AuthenticationError("message");
var actual = Deserialize<AuthenticationError>(_testJson);
actual.Should().BeEquivalentTo(expected);
}
}
@@ -0,0 +1,38 @@
namespace AnthropicClient.Tests.Unit.Models;
public class AutoToolChoiceTests : SerializationTest
{
private readonly string _testJson = @"{
""type"": ""auto""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldSetTypeToAuto()
{
var expected = "auto";
var actual = new AutoToolChoice();
actual.Type.Should().Be(expected);
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var choice = new AutoToolChoice();
var actual = Serialize(choice);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var expected = new AutoToolChoice();
var actual = Deserialize<AutoToolChoice>(_testJson);
actual.Should().BeEquivalentTo(expected);
}
}
@@ -0,0 +1,653 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ChatMessageRequestTests : SerializationTest
{
private readonly string _testJson = @"{
""model"": ""claude-3-sonnet-20240229"",
""system"": ""test-system"",
""messages"": [
{ ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"": ""text"" }] }
],
""max_tokens"": 512,
""metadata"": { ""test"":""test"" },
""stop_sequences"": [],
""temperature"": 0.5,
""topK"": 10,
""topP"": 0.5,
""tool_choice"": { ""type"":""auto"" },
""tools"": [
{
""name"": ""test-tool"",
""description"": ""test-description"",
""input_schema"": {
""type"": ""object"",
""properties"": {
""test-property"": {
""type"": ""string"",
""description"": ""test-description""
}
},
""required"": [""test-property""]
}
}
],
""stream"": false
}";
private readonly string _testJsonWithAnyToolChoice = @"{
""model"": ""claude-3-sonnet-20240229"",
""system"": ""test-system"",
""messages"":[
{ ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"":""text"" }] }
],
""max_tokens"": 512,
""metadata"": {""test"":""test""},
""stop_sequences"": [],
""temperature"": 0.5,
""topK"": 10,
""topP"": 0.5,
""tool_choice"": {""type"":""any""},
""tools"": [
{
""name"": ""test-tool"",
""description"": ""test-description"",
""input_schema"": {
""type"": ""object"",
""properties"": {
""test-property"": {
""type"": ""string"",
""description"": ""test-description""
}
},
""required"": [""test-property""]
}
}
],
""stream"":false
}";
private readonly string _testJsonWithSpecificToolChoice = @"{
""model"": ""claude-3-sonnet-20240229"",
""system"": ""test-system"",
""messages"": [
{ ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"": ""text"" }] }
],
""max_tokens"": 512,
""metadata"": { ""test"": ""test"" },
""stop_sequences"": [],
""temperature"": 0.5,
""topK"": 10,
""topP"": 0.5,
""tool_choice"": { ""type"": ""tool"", ""name"": ""test-tool"" },
""tools"": [
{
""name"": ""test-tool"",
""description"": ""test-description"",
""input_schema"": {
""type"": ""object"",
""properties"": {
""test-property"": {
""type"": ""string"",
""description"": ""test-description""
}
},
""required"": [""test-property""]
}
}
],
""stream"": false
}";
private readonly string _testJsonWithImageContent = @"{
""model"": ""claude-3-sonnet-20240229"",
""system"": ""test-system"",
""messages"":[
{
""role"": ""user"",
""content"": [
{
""type"": ""image"",
""source"": { ""media_type"": ""image/jpeg"", ""data"": ""data"" }
}
]
}
],
""max_tokens"": 512,
""metadata"": { ""test"": ""test"" },
""stop_sequences"": [],
""temperature"": 0.5,
""topK"": 10,
""topP"": 0.5,
""tool_choice"": { ""type"": ""auto"" },
""tools"":[
{
""name"": ""test-tool"",
""description"": ""test-description"",
""input_schema"": {
""type"": ""object"",
""properties"": {
""test-property"": {
""type"": ""string"",
""description"": ""test-description""
}
},
""required"": [""test-property""]
}
}
],
""stream"": false
}";
private readonly string _testJsonWithUnknownContent = @"{
""model"": ""claude-3-sonnet-20240229"",
""system"": ""test-system"",
""messages"": [{ ""role"": ""user"", ""content"": [{ ""type"": ""unknown"", ""text"": ""text"" }] }],
""max_tokens"": 512,
""metadata"": { ""test"": ""test"" },
""stop_sequences"": [],
""temperature"": 0.5,
""topK"": 10,
""topP"": 0.5,
""tool_choice"": { ""type"":""auto"" },
""tools"": [
{
""name"": ""test-tool"",
""description"": ""test-description"",
""input_schema"": {
""type"": ""object"",
""properties"": {
""test-property"": {
""type"": ""string"",
""description"": ""test-description""
}
},
""required"": [""test-property""]
}
}
],
""stream"": false
}";
private readonly string _testJsonWithToolUseContent = @"{
""model"": ""claude-3-sonnet-20240229"",
""system"": ""test-system"",
""messages"": [
{
""role"": ""assistant"",
""content"": [
{
""type"": ""tool_use"",
""name"": ""test-tool"",
""id"": ""test-tool-id"",
""input"": {
""test-property"": ""test-value""
}
}
]
}
],
""max_tokens"": 512,
""metadata"": { ""test"": ""test"" },
""stop_sequences"": [],
""temperature"": 0.5,
""topK"": 10,
""topP"": 0.5,
""tool_choice"": { ""type"": ""auto"" },
""tools"": [
{
""name"": ""test-tool"",
""description"": ""test-description"",
""input_schema"": {
""type"": ""object"",
""properties"": {
""test-property"": {
""type"": ""string"",
""description"": ""test-description""
}
},
""required"": [""test-property""]
}
}
],
""stream"": false
}";
private readonly string _testJsonWithToolResultContent = @"{
""model"": ""claude-3-sonnet-20240229"",
""system"": ""test-system"",
""messages"": [
{
""role"": ""assistant"",
""content"": [
{
""type"": ""tool_result"",
""tool_use_id"": ""test-tool"",
""content"": ""test-value""
}
]
}
],
""max_tokens"": 512,
""metadata"": { ""test"": ""test"" },
""stop_sequences"": [],
""temperature"": 0.5,
""topK"": 10,
""topP"": 0.5,
""tool_choice"": { ""type"": ""auto"" },
""tools"": [
{
""name"": ""test-tool"",
""description"": ""test-description"",
""input_schema"": {
""type"": ""object"",
""properties"": {
""test-property"": {
""type"": ""string"",
""description"": ""test-description""
}
},
""required"": [""test-property""]
}
}
],
""stream"":false
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var model = AnthropicModels.Claude3Sonnet;
var messages = new List<ChatMessage> { new() };
var maxTokens = 512;
var system = "test-system";
var metadata = new Dictionary<string, object> { ["test"] = "test" };
var temperature = 0.5m;
var topK = 10;
var topP = 0.5m;
var toolChoice = new AutoToolChoice();
var tools = new List<Tool> { new() };
var chatMessageRequest = new ChatMessageRequest(
model: model,
messages: messages,
maxTokens: maxTokens,
system: system,
metadata: metadata,
temperature: temperature,
topK: topK,
topP: topP,
toolChoice: toolChoice,
tools: tools
);
chatMessageRequest.Model.Should().Be(model);
chatMessageRequest.Messages.Should().BeSameAs(messages);
chatMessageRequest.MaxTokens.Should().Be(maxTokens);
chatMessageRequest.System.Should().Be(system);
chatMessageRequest.Metadata.Should().BeSameAs(metadata);
chatMessageRequest.Temperature.Should().Be(temperature);
chatMessageRequest.TopK.Should().Be(topK);
chatMessageRequest.TopP.Should().Be(topP);
chatMessageRequest.ToolChoice.Should().Be(toolChoice);
chatMessageRequest.Tools.Should().BeSameAs(tools);
chatMessageRequest.Stream.Should().BeFalse();
}
[Fact]
public void Constructor_WhenCalledAndModelIsNull_ItShouldThrowArgumentNullException()
{
var action = () => new ChatMessageRequest(
model: null!,
messages: [new()]
);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledAndMessagesIsNull_ItShouldThrowArgumentNullException()
{
var action = () => new ChatMessageRequest(
model: AnthropicModels.Claude3Sonnet,
messages: null!
);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledAndModelIsInvalid_ItShouldThrowArgumentException()
{
var action = () => new ChatMessageRequest(
model: "invalid-model",
messages: [new()]
);
action.Should().Throw<ArgumentException>();
}
[Fact]
public void Constructor_WhenCalledAndMessagesIsEmpty_ItShouldThrowArgumentException()
{
var action = () => new ChatMessageRequest(
model: AnthropicModels.Claude3Sonnet,
messages: []
);
action.Should().Throw<ArgumentException>();
}
[Fact]
public void Constructor_WhenCalledAndMaxTokensIsInvalid_ItShouldThrowArgumentException()
{
var action = () => new ChatMessageRequest(
model: AnthropicModels.Claude3Sonnet,
messages: [new()],
maxTokens: 0
);
action.Should().Throw<ArgumentException>();
}
[Theory]
[InlineData(-1)]
[InlineData(2)]
public void Constructor_WhenCalledAndTemperatureIsInvalid_ItShouldThrowArgumentException(decimal temperature)
{
var action = () => new ChatMessageRequest(
model: AnthropicModels.Claude3Sonnet,
messages: [new()],
temperature: temperature
);
action.Should().Throw<ArgumentException>();
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var messages = new List<ChatMessage>()
{
new()
{
Role = MessageRole.User,
Content = [new TextContent("Hello!")]
}
};
var model = AnthropicModels.Claude3Sonnet;
var maxTokens = 512;
var system = "test-system";
var metadata = new Dictionary<string, object>
{
["test"] = "test"
};
var temperature = 0.5m;
var topK = 10;
var topP = 0.5m;
var toolChoice = new AutoToolChoice();
var tools = new List<Tool>
{
new()
{
Name = "test-tool",
Description = "test-description",
InputSchema = new InputSchema(
properties: new Dictionary<string, InputProperty>
{
["test-property"] = new InputProperty(
type: "string",
description: "test-description"
)
},
required: ["test-property"]
),
}
};
var chatMessageRequest = new ChatMessageRequest(
model: model,
messages: messages,
maxTokens: maxTokens,
system: system,
metadata: metadata,
temperature: temperature,
topK: topK,
topP: topP,
toolChoice: toolChoice,
tools: tools
);
var actual = Serialize(chatMessageRequest);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var chatMessageRequest = Deserialize<ChatMessageRequest>(_testJson);
chatMessageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
chatMessageRequest.System.Should().Be("test-system");
chatMessageRequest.Messages.Should().HaveCount(1);
chatMessageRequest.MaxTokens.Should().Be(512);
chatMessageRequest.Metadata.Should().HaveCount(1);
var testValue = chatMessageRequest.Metadata!.GetValueOrDefault("test")!.ToString();
testValue.Should().Be("test");
chatMessageRequest.Temperature.Should().Be(0.5m);
chatMessageRequest.TopK.Should().Be(10);
chatMessageRequest.TopP.Should().Be(0.5m);
chatMessageRequest.ToolChoice.Should().BeOfType<AutoToolChoice>();
chatMessageRequest.ToolChoice!.Type.Should().Be("auto");
chatMessageRequest.Tools.Should().HaveCount(1);
chatMessageRequest.Tools![0].Name.Should().Be("test-tool");
chatMessageRequest.Tools[0].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Type.Should().Be("object");
chatMessageRequest.Tools[0].InputSchema.Properties.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Type.Should().Be("string");
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Required.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Required[0].Should().Be("test-property");
chatMessageRequest.Stream.Should().BeFalse();
}
[Fact]
public void JsonDeserialization_WhenDeserializedWithAnyToolChoice_ItShouldHaveExpectedShape()
{
var chatMessageRequest = Deserialize<ChatMessageRequest>(_testJsonWithAnyToolChoice);
chatMessageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
chatMessageRequest.System.Should().Be("test-system");
chatMessageRequest.Messages.Should().HaveCount(1);
chatMessageRequest.MaxTokens.Should().Be(512);
chatMessageRequest.Metadata.Should().HaveCount(1);
var testValue = chatMessageRequest.Metadata!.GetValueOrDefault("test")!.ToString();
testValue.Should().Be("test");
chatMessageRequest.Temperature.Should().Be(0.5m);
chatMessageRequest.TopK.Should().Be(10);
chatMessageRequest.TopP.Should().Be(0.5m);
chatMessageRequest.ToolChoice.Should().BeOfType<AnyToolChoice>();
chatMessageRequest.ToolChoice!.Type.Should().Be("any");
chatMessageRequest.Tools.Should().HaveCount(1);
chatMessageRequest.Tools![0].Name.Should().Be("test-tool");
chatMessageRequest.Tools[0].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Type.Should().Be("object");
chatMessageRequest.Tools[0].InputSchema.Properties.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Type.Should().Be("string");
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Required.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Required[0].Should().Be("test-property");
}
[Fact]
public void JsonDeserialization_WhenDeserializedWithSpecificToolChoice_ItShouldHaveExpectedShape()
{
var chatMessageRequest = Deserialize<ChatMessageRequest>(_testJsonWithSpecificToolChoice);
chatMessageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
chatMessageRequest.System.Should().Be("test-system");
chatMessageRequest.Messages.Should().HaveCount(1);
chatMessageRequest.MaxTokens.Should().Be(512);
chatMessageRequest.Metadata.Should().HaveCount(1);
var testValue = chatMessageRequest.Metadata!.GetValueOrDefault("test")!.ToString();
testValue.Should().Be("test");
chatMessageRequest.Temperature.Should().Be(0.5m);
chatMessageRequest.TopK.Should().Be(10);
chatMessageRequest.TopP.Should().Be(0.5m);
chatMessageRequest.ToolChoice.Should().BeOfType<SpecificToolChoice>();
var specificToolChoice = chatMessageRequest.ToolChoice as SpecificToolChoice;
specificToolChoice!.Type.Should().Be("tool");
specificToolChoice.Name.Should().Be("test-tool");
chatMessageRequest.Tools.Should().HaveCount(1);
chatMessageRequest.Tools![0].Name.Should().Be("test-tool");
chatMessageRequest.Tools[0].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Type.Should().Be("object");
chatMessageRequest.Tools[0].InputSchema.Properties.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Type.Should().Be("string");
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Required.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Required[0].Should().Be("test-property");
}
[Fact]
public void JsonDeserialization_WhenDeserializedWithUnknownToolChoice_ItShouldThrowJsonException()
{
var json = @"{""model"":""claude-3-sonnet-20240229"",""system"":""test-system"",""messages"":[{""role"":""user"",""content"":[{""text"":""Hello!"",""type"":""text""}]}],""max_tokens"":512,""metadata"":{""test"":""test""},""stop_sequences"":[],""temperature"":0.5,""topK"":10,""topP"":0.5,""tool_choice"":{""type"":""unknown""},""tools"":[{""name"":""test-tool"",""description"":""test-description"",""input_schema"":{""type"":""object"",""properties"":{""test-property"":{""type"":""string"",""description"":""test-description""}},""required"":[""test-property""]}}],""stream"":false}";
var action = () => Deserialize<ChatMessageRequest>(json);
action.Should().Throw<JsonException>();
}
[Fact]
public void JsonDeserialization_WhenDeserializedWithImageContent_ItShouldHaveExpectedShape()
{
var chatMessageRequest = Deserialize<ChatMessageRequest>(_testJsonWithImageContent);
chatMessageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
chatMessageRequest.System.Should().Be("test-system");
chatMessageRequest.Messages.Should().HaveCount(1);
chatMessageRequest.MaxTokens.Should().Be(512);
chatMessageRequest.Metadata.Should().HaveCount(1);
var testValue = chatMessageRequest.Metadata!.GetValueOrDefault("test")!.ToString();
testValue.Should().Be("test");
chatMessageRequest.Temperature.Should().Be(0.5m);
chatMessageRequest.TopK.Should().Be(10);
chatMessageRequest.TopP.Should().Be(0.5m);
chatMessageRequest.ToolChoice.Should().BeOfType<AutoToolChoice>();
chatMessageRequest.ToolChoice!.Type.Should().Be("auto");
chatMessageRequest.Tools.Should().HaveCount(1);
chatMessageRequest.Tools![0].Name.Should().Be("test-tool");
chatMessageRequest.Tools[0].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Type.Should().Be("object");
chatMessageRequest.Tools[0].InputSchema.Properties.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Type.Should().Be("string");
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Required.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Required[0].Should().Be("test-property");
chatMessageRequest.Messages[0].Content.Should().HaveCount(1);
chatMessageRequest.Messages[0].Content[0].Should().BeOfType<ImageContent>();
var imageContent = chatMessageRequest.Messages[0].Content[0] as ImageContent;
imageContent!.Type.Should().Be("image");
imageContent.Source.MediaType.Should().Be("image/jpeg");
imageContent.Source.Data.Should().Be("data");
}
[Fact]
public void JsonDeserialization_WhenDeserializedWithToolUseContent_ItShouldHaveExpectedShape()
{
var chatMessageRequest = Deserialize<ChatMessageRequest>(_testJsonWithToolUseContent);
chatMessageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
chatMessageRequest.System.Should().Be("test-system");
chatMessageRequest.Messages.Should().HaveCount(1);
chatMessageRequest.MaxTokens.Should().Be(512);
chatMessageRequest.Metadata.Should().HaveCount(1);
var testValue = chatMessageRequest.Metadata!.GetValueOrDefault("test")!.ToString();
testValue.Should().Be("test");
chatMessageRequest.Temperature.Should().Be(0.5m);
chatMessageRequest.TopK.Should().Be(10);
chatMessageRequest.TopP.Should().Be(0.5m);
chatMessageRequest.ToolChoice.Should().BeOfType<AutoToolChoice>();
chatMessageRequest.ToolChoice!.Type.Should().Be("auto");
chatMessageRequest.Tools.Should().HaveCount(1);
chatMessageRequest.Tools![0].Name.Should().Be("test-tool");
chatMessageRequest.Tools[0].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Type.Should().Be("object");
chatMessageRequest.Tools[0].InputSchema.Properties.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Type.Should().Be("string");
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Required.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Required[0].Should().Be("test-property");
chatMessageRequest.Messages[0].Content.Should().HaveCount(1);
chatMessageRequest.Messages[0].Content[0].Should().BeOfType<ToolUseContent>();
var toolUseContent = chatMessageRequest.Messages[0].Content[0] as ToolUseContent;
toolUseContent!.Type.Should().Be("tool_use");
toolUseContent.Name.Should().Be("test-tool");
toolUseContent.Id.Should().Be("test-tool-id");
toolUseContent.Input.Should().HaveCount(1);
toolUseContent.Input["test-property"]!.ToString().Should().Be("test-value");
}
[Fact]
public void JsonDeserialization_WhenDeserializedWithToolResultContent_ItShouldHaveExpectedShape()
{
var chatMessageRequest = Deserialize<ChatMessageRequest>(_testJsonWithToolResultContent);
chatMessageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet);
chatMessageRequest.System.Should().Be("test-system");
chatMessageRequest.Messages.Should().HaveCount(1);
chatMessageRequest.MaxTokens.Should().Be(512);
chatMessageRequest.Metadata.Should().HaveCount(1);
var testValue = chatMessageRequest.Metadata!.GetValueOrDefault("test")!.ToString();
testValue.Should().Be("test");
chatMessageRequest.Temperature.Should().Be(0.5m);
chatMessageRequest.TopK.Should().Be(10);
chatMessageRequest.TopP.Should().Be(0.5m);
chatMessageRequest.ToolChoice.Should().BeOfType<AutoToolChoice>();
chatMessageRequest.ToolChoice!.Type.Should().Be("auto");
chatMessageRequest.Tools.Should().HaveCount(1);
chatMessageRequest.Tools![0].Name.Should().Be("test-tool");
chatMessageRequest.Tools[0].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Type.Should().Be("object");
chatMessageRequest.Tools[0].InputSchema.Properties.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Type.Should().Be("string");
chatMessageRequest.Tools[0].InputSchema.Properties["test-property"].Description.Should().Be("test-description");
chatMessageRequest.Tools[0].InputSchema.Required.Should().HaveCount(1);
chatMessageRequest.Tools[0].InputSchema.Required[0].Should().Be("test-property");
chatMessageRequest.Messages[0].Content.Should().HaveCount(1);
chatMessageRequest.Messages[0].Content[0].Should().BeOfType<ToolResultContent>();
var toolResultContent = chatMessageRequest.Messages[0].Content[0] as ToolResultContent;
toolResultContent!.Type.Should().Be("tool_result");
toolResultContent.ToolUseId.Should().Be("test-tool");
toolResultContent.Content.Should().Be("test-value");
}
[Fact]
public void JsonDeserialization_WhenDeserializedWithUnknownContent_ItShouldThrowJsonException()
{
var action = () => Deserialize<ChatMessageRequest>(_testJsonWithUnknownContent);
action.Should().Throw<JsonException>();
}
}
@@ -0,0 +1,78 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ChatMessageTests : SerializationTest
{
private readonly string _testJson = @"{
""role"": ""assistant"",
""content"": [
{ ""text"": ""text"", ""type"": ""text"" }
]
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var role = "assistant";
var content = new List<Content> { new TextContent("text") };
var chatMessage = new ChatMessage(role, content);
chatMessage.Role.Should().Be(role);
chatMessage.Content.Should().BeSameAs(content);
}
[Fact]
public void Constructor_WhenCalledAndRoleIsNull_ItShouldThrowArgumentNullException()
{
var content = new List<Content> { new TextContent("text") };
var action = () => new ChatMessage(null!, content);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledAndContentIsNull_ItShouldThrowArgumentNullException()
{
var role = "assistant";
var action = () => new ChatMessage(role, null!);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledAndRoleIsInvalid_ItShouldThrowArgumentException()
{
var role = "invalid";
var content = new List<Content> { new TextContent("text") };
var action = () => new ChatMessage(role, content);
action.Should().Throw<ArgumentException>();
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldReturnJsonString()
{
var role = "assistant";
var content = new List<Content> { new TextContent("text") };
var chatMessage = new ChatMessage(role, content);
var actual = Serialize(chatMessage);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldReturnChatMessage()
{
var chatMessage = Deserialize<ChatMessage>(_testJson);
chatMessage.Should().NotBeNull();
chatMessage!.Role.Should().Be("assistant");
chatMessage.Content.Should().HaveCount(1);
chatMessage.Content[0].Should().BeOfType<TextContent>();
chatMessage.Content[0].As<TextContent>().Text.Should().Be("text");
}
}
@@ -0,0 +1,120 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ChatResponseTests : SerializationTest
{
[Fact]
public void Constructor_WhenCalled_ShouldInitializeProperties()
{
var expectedId = "id";
var expectedModel = "model";
var expectedRole = "role";
var expectedStopReason = "stop reason";
var expectedStopSequence = "stop sequence";
var expectedType = "type";
var expectedUsage = new ChatUsage
{
InputTokens = 1,
OutputTokens = 2
};
var expectedContent = new List<Content>
{
new TextContent("text content"),
};
var chatResponse = new ChatResponse
{
Id = expectedId,
Model = expectedModel,
Role = expectedRole,
StopReason = expectedStopReason,
StopSequence = expectedStopSequence,
Type = expectedType,
Usage = expectedUsage,
Content = expectedContent
};
chatResponse.Id.Should().Be(expectedId);
chatResponse.Model.Should().Be(expectedModel);
chatResponse.Role.Should().Be(expectedRole);
chatResponse.StopReason.Should().Be(expectedStopReason);
chatResponse.StopSequence.Should().Be(expectedStopSequence);
chatResponse.Type.Should().Be(expectedType);
chatResponse.Usage.Should().BeEquivalentTo(expectedUsage);
chatResponse.Content.Should().BeEquivalentTo(expectedContent);
}
[Fact]
public void JsonSerialization_WhenCalled_ItShouldSerializeCorrectly()
{
var expectedJson = @"{
""id"": ""id"",
""model"": ""model"",
""role"": ""role"",
""stop_reason"": ""stop reason"",
""stop_sequence"": ""stop sequence"",
""type"": ""type"",
""usage"": { ""input_tokens"": 1, ""output_tokens"": 2 },
""content"": [
{ ""text"": ""text content"", ""type"": ""text"" }
]
}";
var chatResponse = new ChatResponse
{
Id = "id",
Model = "model",
Role = "role",
StopReason = "stop reason",
StopSequence = "stop sequence",
Type = "type",
Usage = new ChatUsage
{
InputTokens = 1,
OutputTokens = 2
},
Content =
[
new TextContent("text content"),
]
};
var actual = Serialize(chatResponse);
JsonAssert.Equal(expectedJson, actual);
}
[Fact]
public void JsonDeserialization_WhenCalled_ItShouldDeserializeCorrectly()
{
var json = @"{
""id"": ""id"",
""model"": ""model"",
""role"": ""role"",
""stop_reason"": ""stop reason"",
""stop_sequence"": ""stop sequence"",
""type"": ""type"",
""usage"": { ""input_tokens"": 1, ""output_tokens"": 2 },
""content"": [
{ ""text"": ""text content"", ""type"": ""text"" }
]
}";
var chatResponse = Deserialize<ChatResponse>(json);
chatResponse!.Id.Should().Be("id");
chatResponse.Model.Should().Be("model");
chatResponse.Role.Should().Be("role");
chatResponse.StopReason.Should().Be("stop reason");
chatResponse.StopSequence.Should().Be("stop sequence");
chatResponse.Type.Should().Be("type");
chatResponse.Usage.Should().BeEquivalentTo(new ChatUsage
{
InputTokens = 1,
OutputTokens = 2
});
chatResponse.Content.Should().BeEquivalentTo(new List<Content>
{
new TextContent("text content"),
});
}
}
@@ -0,0 +1,47 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ChatUsageTests : SerializationTest
{
[Fact]
public void Constructor_WhenCalled_ShouldInitializeProperties()
{
var expectedInputTokens = 1;
var expectedOutputTokens = 2;
var chatUsage = new ChatUsage
{
InputTokens = expectedInputTokens,
OutputTokens = expectedOutputTokens
};
chatUsage.InputTokens.Should().Be(expectedInputTokens);
chatUsage.OutputTokens.Should().Be(expectedOutputTokens);
}
[Fact]
public void JsonSerialization_WhenCalled_ItShouldSerializeCorrectly()
{
var expectedJson = @"{ ""input_tokens"": 1, ""output_tokens"": 2 }";
var chatUsage = new ChatUsage
{
InputTokens = 1,
OutputTokens = 2
};
var actual = Serialize(chatUsage);
JsonAssert.Equal(expectedJson, actual);
}
[Fact]
public void JsonDeserialization_WhenCalled_ItShouldDeserializeCorrectly()
{
var json = @"{ ""input_tokens"": 1, ""output_tokens"": 2 }";
var chatUsage = Deserialize<ChatUsage>(json);
chatUsage!.InputTokens.Should().Be(1);
chatUsage.OutputTokens.Should().Be(2);
}
}
@@ -0,0 +1,44 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ContentTypeTests
{
[Fact]
public void Text_WhenCalled_ItShouldReturnText()
{
var expected = "text";
var actual = ContentType.Text;
actual.Should().Be(expected);
}
[Fact]
public void Image_WhenCalled_ItShouldReturnImage()
{
var expected = "image";
var actual = ContentType.Image;
actual.Should().Be(expected);
}
[Fact]
public void ToolUse_WhenCalled_ItShouldReturnToolUse()
{
var expected = "tool_use";
var actual = ContentType.ToolUse;
actual.Should().Be(expected);
}
[Fact]
public void ToolResult_WhenCalled_ItShouldReturnToolResult()
{
var expected = "tool_result";
var actual = ContentType.ToolResult;
actual.Should().Be(expected);
}
}
@@ -0,0 +1,74 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ImageContentTests : SerializationTest
{
private readonly string _testJson = @"{
""source"": {
""media_type"": ""image/png"",
""data"": ""data""
},
""type"": ""image""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeSource()
{
var expectedMediaType = "image/png";
var expectedData = "data";
var result = new ImageContent(expectedMediaType, expectedData);
result.Source.Should().BeEquivalentTo(new ImageSource(expectedMediaType, expectedData));
}
[Fact]
public void Constructor_WhenCalledAndMediaTypeIsNull_ItShouldThrowArgumentNullException()
{
var expectedData = "data";
var action = () => new ImageContent(null!, expectedData);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledAndDataIsNull_ItShouldThrowArgumentNullException()
{
var expectedMediaType = "image/png";
var action = () => new ImageContent(expectedMediaType, null!);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledWithInvalidMediaType_ItShouldThrowArgumentException()
{
var expectedMediaType = "invalid";
var expectedData = "data";
var action = () => new ImageContent(expectedMediaType, expectedData);
action.Should().Throw<ArgumentException>();
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var content = new ImageContent("image/png", "data");
var actual = Serialize(content);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var expected = new ImageContent("image/png", "data");
var actual = Deserialize<ImageContent>(_testJson);
actual.Should().BeEquivalentTo(expected);
}
}
@@ -0,0 +1,57 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ImageTypeTests
{
[Fact]
public void Jpg_WhenCalled_ItShouldReturnJpg()
{
var expected = "image/jpeg";
var actual = ImageType.Jpg;
actual.Should().Be(expected);
}
[Fact]
public void Png_WhenCalled_ItShouldReturnPng()
{
var expected = "image/png";
var actual = ImageType.Png;
actual.Should().Be(expected);
}
[Fact]
public void Gif_WhenCalled_ItShouldReturnGif()
{
var expected = "image/gif";
var actual = ImageType.Gif;
actual.Should().Be(expected);
}
[Fact]
public void Webp_WhenCalled_ItShouldReturnWebp()
{
var expected = "image/webp";
var actual = ImageType.Webp;
actual.Should().Be(expected);
}
[Theory]
[InlineData("image/jpeg", true)]
[InlineData("image/png", true)]
[InlineData("image/gif", true)]
[InlineData("image/webp", true)]
[InlineData("invalid", false)]
public void IsValidImageType_WhenCalled_ItShouldReturnExpectedValue(string imageType, bool expected)
{
var actual = ImageType.IsValidImageType(imageType);
actual.Should().Be(expected);
}
}
@@ -0,0 +1,60 @@
namespace AnthropicClient.Tests.Unit.Models;
public class InputPropertyTests : SerializationTest
{
private readonly string _testJson = @"{
""type"": ""type"",
""description"": ""description""
}";
[Fact]
public void Constructor_WhenCalled_ShouldInitializeProperties()
{
var type = "type";
var description = "description";
var inputProperty = new InputProperty(type, description);
inputProperty.Type.Should().Be(type);
inputProperty.Description.Should().Be(description);
}
[Fact]
public void Constructor_WhenTypeIsNull_ShouldThrowArgumentNullException()
{
var description = "description";
var action = () => new InputProperty(null!, description);
action.Should().Throw<ArgumentNullException>().WithMessage("Value cannot be null. (Parameter 'type')");
}
[Fact]
public void Constructor_WhenDescriptionIsNull_ShouldThrowArgumentNullException()
{
var type = "type";
var action = () => new InputProperty(type, null!);
action.Should().Throw<ArgumentNullException>().WithMessage("Value cannot be null. (Parameter 'description')");
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var inputProperty = new InputProperty("type", "description");
var json = Serialize(inputProperty);
JsonAssert.Equal(_testJson, json);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var inputProperty = Deserialize<InputProperty>(_testJson);
inputProperty!.Type.Should().Be("type");
inputProperty.Description.Should().Be("description");
}
}
@@ -0,0 +1,116 @@
namespace AnthropicClient.Tests.Unit.Models;
public class InputSchemaTests : SerializationTest
{
private readonly string _testJson = @$"{{
""type"": ""object"",
""properties"": {{
""property1"": {{ ""type"": ""type1"", ""description"": ""description1"" }},
""property2"": {{ ""type"": ""type2"", ""description"": ""description2"" }}
}},
""required"": [""property1""]
}}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var inputProperties = new Dictionary<string, InputProperty>
{
{ "property1", new InputProperty("type1", "description1") },
{ "property2", new InputProperty("type2", "description2") }
};
var required = new List<string> { "property1" };
var inputSchema = new InputSchema(
properties: inputProperties,
required: required
);
inputSchema.Type.Should().Be("object");
inputSchema.Properties.Should().BeSameAs(inputProperties);
inputSchema.Required.Should().BeSameAs(required);
}
[Fact]
public void Constructor_WhenPropertiesIsNull_ItShouldThrowArgumentNullException()
{
var required = new List<string> { "property1" };
var action = () => new InputSchema(null!, required);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenRequiredIsNull_ItShouldThrowArgumentNullException()
{
var inputProperties = new Dictionary<string, InputProperty>
{
{ "property1", new InputProperty("type1", "description1") },
{ "property2", new InputProperty("type2", "description2") }
};
var action = () => new InputSchema(inputProperties, null!);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenRequiredPropertyIsNotInProperties_ItShouldThrowArgumentException()
{
var inputProperties = new Dictionary<string, InputProperty>
{
{ "property1", new InputProperty("type1", "description1") },
{ "property2", new InputProperty("type2", "description2") }
};
var required = new List<string> { "property3" };
var action = () => new InputSchema(inputProperties, required);
action.Should().Throw<ArgumentException>();
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var inputProperties = new Dictionary<string, InputProperty>
{
{ "property1", new InputProperty("type1", "description1") },
{ "property2", new InputProperty("type2", "description2") }
};
var required = new List<string> { "property1" };
var inputSchema = new InputSchema(
properties: inputProperties,
required: required
);
var actual = Serialize(inputSchema);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var inputProperties = new Dictionary<string, InputProperty>
{
{ "property1", new InputProperty("type1", "description1") },
{ "property2", new InputProperty("type2", "description2") }
};
var required = new List<string> { "property1" };
var expected = new InputSchema(
properties: inputProperties,
required: required
);
var actual = Deserialize<InputSchema>(_testJson);
actual.Should().BeEquivalentTo(expected);
}
}
@@ -0,0 +1,47 @@
namespace AnthropicClient.Tests.Unit.Models;
public class InvalidRequestErrorTests : SerializationTest
{
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var message = "message";
var error = new InvalidRequestError(message);
error.Type.Should().Be("invalid_request_error");
error.Message.Should().Be(message);
}
[Fact]
public void Serialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var message = "message";
var error = new InvalidRequestError(message);
var serialized = Serialize(error);
JsonAssert.Equal(
@"{
""type"": ""invalid_request_error"",
""message"": ""message""
}",
serialized
);
}
[Fact]
public void Deserialization_WhenDeserialized_ItShouldHaveExpectedProperties()
{
var json = @"{
""type"": ""invalid_request_error"",
""message"": ""message""
}";
var error = Deserialize<InvalidRequestError>(json);
error!.Type.Should().Be("invalid_request_error");
error.Message.Should().Be("message");
}
}
@@ -0,0 +1,35 @@
namespace AnthropicClient.Tests.Unit.Models;
public class MessageRoleTests
{
[Fact]
public void User_WhenCalled_ItShouldReturnUser()
{
var expected = "user";
var actual = MessageRole.User;
actual.Should().Be(expected);
}
[Fact]
public void Assistant_WhenCalled_ItShouldReturnAssistant()
{
var expected = "assistant";
var actual = MessageRole.Assistant;
actual.Should().Be(expected);
}
[Theory]
[InlineData("user", true)]
[InlineData("assistant", true)]
[InlineData("invalid", false)]
public void IsValidRole_WhenCalled_ItShouldReturnExpectedValue(string role, bool expected)
{
var actual = MessageRole.IsValidRole(role);
actual.Should().Be(expected);
}
}
@@ -0,0 +1,40 @@
namespace AnthropicClient.Tests.Unit.Models;
public class NotFoundErrorTests : SerializationTest
{
private readonly string _testJson = @"{
""message"": ""message"",
""type"": ""not_found_error""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var expectedMessage = "message";
var actual = new NotFoundError(expectedMessage);
actual.Message.Should().Be(expectedMessage);
actual.Type.Should().Be("not_found_error");
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var error = new NotFoundError("message");
var actual = Serialize(error);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var expected = new NotFoundError("message");
var actual = Deserialize<NotFoundError>(_testJson);
actual.Should().BeEquivalentTo(expected);
}
}
@@ -0,0 +1,40 @@
namespace AnthropicClient.Tests.Unit.Models;
public class OverloadedErrorTests : SerializationTest
{
private readonly string _testJson = @"{
""message"": ""message"",
""type"": ""overloaded_error""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var expectedMessage = "message";
var actual = new OverloadedError(expectedMessage);
actual.Message.Should().Be(expectedMessage);
actual.Type.Should().Be("overloaded_error");
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var error = new OverloadedError("message");
var actual = Serialize(error);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var expected = new OverloadedError("message");
var actual = Deserialize<OverloadedError>(_testJson);
actual.Should().BeEquivalentTo(expected);
}
}
@@ -0,0 +1,40 @@
namespace AnthropicClient.Tests.Unit.Models;
public class PermissionErrorTests : SerializationTest
{
private readonly string _testJson = @"{
""message"": ""message"",
""type"": ""permission_error""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var expectedMessage = "message";
var actual = new PermissionError(expectedMessage);
actual.Message.Should().Be(expectedMessage);
actual.Type.Should().Be("permission_error");
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var error = new PermissionError("message");
var actual = Serialize(error);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var expected = new PermissionError("message");
var actual = Deserialize<PermissionError>(_testJson);
actual.Should().BeEquivalentTo(expected);
}
}
@@ -0,0 +1,40 @@
namespace AnthropicClient.Tests.Unit.Models;
public class RateLimitErrorTests : SerializationTest
{
private readonly string _testJson = @"{
""message"": ""message"",
""type"": ""rate_limit_error""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var expectedMessage = "message";
var actual = new RateLimitError(expectedMessage);
actual.Message.Should().Be(expectedMessage);
actual.Type.Should().Be("rate_limit_error");
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var error = new RateLimitError("message");
var actual = Serialize(error);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var expected = new RateLimitError("message");
var actual = Deserialize<RateLimitError>(_testJson);
actual.Should().BeEquivalentTo(expected);
}
}
@@ -0,0 +1,51 @@
namespace AnthropicClient.Tests.Unit.Models;
public class SpecificToolChoiceTests : SerializationTest
{
private readonly string _testToolName = "tool";
private string GetTestJson(string name) => @$"{{
""name"": ""{name}"",
""type"": ""tool""
}}";
[Fact]
public void Constructor_WhenCalled_ItShouldSetTypeToTool()
{
var expectedType = "tool";
var actual = new SpecificToolChoice(_testToolName);
actual.Name.Should().Be(_testToolName);
actual.Type.Should().Be(expectedType);
}
[Fact]
public void Constructor_WhenCalledWithNull_ItShouldThrowArgumentNullException()
{
var action = () => new SpecificToolChoice(null!);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var expected = GetTestJson(_testToolName);
var choice = new SpecificToolChoice(_testToolName);
var actual = Serialize(choice);
JsonAssert.Equal(expected, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var expected = new SpecificToolChoice(_testToolName);
var actual = Deserialize<SpecificToolChoice>(GetTestJson(_testToolName));
actual.Should().BeEquivalentTo(expected);
}
}
@@ -0,0 +1,44 @@
namespace AnthropicClient.Tests.Unit.Models;
public class StopReasonTypeTests
{
[Fact]
public void EndTurn_WhenCalled_ItShouldReturnEndTurn()
{
var expected = "end_turn";
var actual = StopReasonType.EndTurn;
actual.Should().Be(expected);
}
[Fact]
public void MaxTokens_WhenCalled_ItShouldReturnMaxTokens()
{
var expected = "max_tokens";
var actual = StopReasonType.MaxTokens;
actual.Should().Be(expected);
}
[Fact]
public void StopSequence_WhenCalled_ItShouldReturnStopSequence()
{
var expected = "stop_sequence";
var actual = StopReasonType.StopSequence;
actual.Should().Be(expected);
}
[Fact]
public void ToolUse_WhenCalled_ItShouldReturnToolUse()
{
var expected = "tool_use";
var actual = StopReasonType.ToolUse;
actual.Should().Be(expected);
}
}
@@ -0,0 +1,44 @@
namespace AnthropicClient.Tests.Unit.Models;
public class TextContentTests : SerializationTest
{
private readonly string _testJson = @"{ ""text"": ""text"", ""type"": ""text"" }";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeText()
{
var expectedText = "text";
var result = new TextContent(expectedText);
result.Text.Should().Be(expectedText);
}
[Fact]
public void Constructor_WhenCalledAndTextIsNull_ItShouldThrowArgumentNullException()
{
var action = () => new TextContent(null!);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var content = new TextContent("text");
var actual = Serialize(content);
JsonAssert.Equal(_testJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var expected = new TextContent("text");
var actual = Deserialize<TextContent>(_testJson);
actual.Should().BeEquivalentTo(expected);
}
}
@@ -0,0 +1,46 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ToolChoiceTypeTests
{
[Fact]
public void Auto_WhenCalled_ItShouldReturnAuto()
{
var expected = "auto";
var actual = ToolChoiceType.Auto;
actual.Should().Be(expected);
}
[Fact]
public void Any_WhenCalled_ItShouldReturnAny()
{
var expected = "any";
var actual = ToolChoiceType.Any;
Assert.Equal(expected, actual);
}
[Fact]
public void Tool_WhenCalled_ItShouldReturnTool()
{
var expected = "tool";
var actual = ToolChoiceType.Tool;
Assert.Equal(expected, actual);
}
[Theory]
[InlineData("auto", true)]
[InlineData("any", true)]
[InlineData("tool", true)]
[InlineData("invalid", false)]
public void IsValidType_WhenCalledWithType_ItShouldReturnExpectedResult(string type, bool expected)
{
var actual = ToolChoiceType.IsValidType(type);
Assert.Equal(expected, actual);
}
}
@@ -0,0 +1,79 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ToolResultContentTests : SerializationTest
{
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var toolUseId = Guid.NewGuid().ToString();
var content = "content";
var actual = new ToolResultContent(toolUseId, content);
actual.ToolUseId.Should().Be(toolUseId);
actual.Content.Should().Be(content);
actual.Type.Should().Be("tool_result");
}
[Fact]
public void Constructor_WhenCalledAndToolUseIdIsNull_ItShouldThrowArgumentNullException()
{
var content = "content";
var action = () => new ToolResultContent(null!, content);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledAndContentIsNull_ItShouldThrowArgumentNullException()
{
var toolUseId = Guid.NewGuid().ToString();
var action = () => new ToolResultContent(toolUseId, null!);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldReturnJsonString()
{
var toolUseId = Guid.NewGuid().ToString();
var content = "content";
var expectedJson = @$"{{
""tool_use_id"": ""{toolUseId}"",
""content"": ""{content}"",
""type"": ""tool_result""
}}";
var toolResultContent = new ToolResultContent
{
ToolUseId = toolUseId,
Content = content
};
var actual = Serialize(toolResultContent);
JsonAssert.Equal(expectedJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldReturnToolResultContent()
{
var toolUseId = Guid.NewGuid().ToString();
var content = "content";
var json = @$"{{
""tool_use_id"": ""{toolUseId}"",
""content"": ""{content}"",
""type"": ""tool_result""
}}";
var actual = Deserialize<ToolResultContent>(json);
actual!.ToolUseId.Should().Be(toolUseId);
actual.Content.Should().Be(content);
actual.Type.Should().Be("tool_result");
}
}
@@ -0,0 +1,88 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ToolTests : SerializationTest
{
private readonly string _testJson = @"{
""name"": ""test-name"",
""description"": ""test-description"",
""input_schema"": { ""type"": ""object"", ""properties"": {}, ""required"": [] }
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var name = "test-name";
var description = "test-description";
var inputSchema = new InputSchema();
var tool = new Tool(
name: name,
description: description,
inputSchema: inputSchema
);
tool.Name.Should().Be(name);
tool.Description.Should().Be(description);
tool.InputSchema.Should().Be(inputSchema);
}
[Fact]
public void Constructor_WhenCalledAndNameIsNull_ItShouldThrowArgumentNullException()
{
var action = () => new Tool(
name: null!,
description: "test-description",
inputSchema: new()
);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledAndDescriptionIsNull_ItShouldThrowArgumentNullException()
{
var action = () => new Tool(
name: "test-name",
description: null!,
inputSchema: new()
);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledAndInputSchemaIsNull_ItShouldThrowArgumentNullException()
{
var action = () => new Tool(
name: "test-name",
description: "test-description",
inputSchema: null!
);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
{
var tool = new Tool(
name: "test-name",
description: "test-description",
inputSchema: new()
);
var json = Serialize(tool);
JsonAssert.Equal(_testJson, json);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape()
{
var tool = Deserialize<Tool>(_testJson);
tool!.Name.Should().Be("test-name");
tool.Description.Should().Be("test-description");
tool.InputSchema.Should().BeEquivalentTo(new InputSchema());
}
}
@@ -0,0 +1,83 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ToolUseContentTests : SerializationTest
{
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var id = Guid.NewGuid().ToString();
var name = "name";
var input = new Dictionary<string, object?> { { "name", "input" } };
var actual = new ToolUseContent()
{
Id = id,
Name = name,
Input = input
};
actual.Id.Should().Be(id);
actual.Name.Should().Be(name);
actual.Input.Should().BeEquivalentTo(input);
actual.Type.Should().Be("tool_use");
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldReturnJsonString()
{
var id = Guid.NewGuid().ToString();
var name = "name";
var input = new Dictionary<string, object?> { { "name", "input" } };
var expectedJson = @$"{{
""id"": ""{id}"",
""name"": ""{name}"",
""input"": {{ ""name"": ""input"" }},
""type"": ""tool_use""
}}";
var toolUseContent = new ToolUseContent()
{
Id = id,
Name = name,
Input = input
};
var actual = Serialize(toolUseContent);
JsonAssert.Equal(expectedJson, actual);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldReturnToolUseContent()
{
var id = Guid.NewGuid().ToString();
var name = "name";
var input = new Dictionary<string, object?> { { "name", "input" } };
var json = @$"{{
""id"": ""{id}"",
""name"": ""{name}"",
""input"": {{ ""name"": ""input"" }},
""type"": ""tool_use""
}}";
var expected = new ToolUseContent()
{
Id = id,
Name = name,
Input = input
};
var actual = Deserialize<ToolUseContent>(json);
actual!.Id.Should().Be(expected.Id);
actual.Name.Should().Be(expected.Name);
var actualInput = actual.Input.GetValueOrDefault("name")!.ToString();
var expectedInput = expected.Input.GetValueOrDefault("name")!.ToString();
actualInput.Should().Be(expectedInput);
actual.Type.Should().Be(expected.Type);
}
}
@@ -0,0 +1,12 @@
using AnthropicClient.Json;
namespace AnthropicClient.Tests.Unit;
public class SerializationTest
{
private readonly JsonSerializerOptions _jsonSerializerOptions = JsonSerializationOptions.DefaultOptions;
protected string Serialize<T>(T obj) => JsonSerializer.Serialize(obj, _jsonSerializerOptions);
protected T? Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json, _jsonSerializerOptions);
}
@@ -0,0 +1,24 @@
namespace AnthropicClient.Tests.Unit.Utils;
public class ArgumentValidatorTests
{
[Fact]
public void ThrowIfNull_WhenValueIsNull_ItShouldThrowArgumentNullException()
{
object? value = null;
var action = () => ArgumentValidator.ThrowIfNull(value, "value");
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void ThrowIfNull_WhenValueIsNotNull_ItShouldNotThrow()
{
var value = new object();
var action = () => ArgumentValidator.ThrowIfNull(value, "value");
action.Should().NotThrow();
}
}
+9
View File
@@ -0,0 +1,9 @@
global using FluentAssertions;
global using AnthropicClient.Models;
global using AnthropicClient.Utils;
global using System.Text.Json;
global using System.Text.Json.JsonDiffPatch.Xunit;
global using Microsoft.Extensions.Configuration;