Compare commits
22
Commits
copilot/fix-35
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
071c9c133b | ||
|
|
b859859548 | ||
|
|
21ce029f09 | ||
|
|
723476b18f | ||
|
|
1f83899eb8 | ||
|
|
267972ca94 | ||
|
|
7657e661d0 | ||
|
|
b48baffb60 | ||
|
|
ad3b990138 | ||
|
|
ea4c8230bc | ||
|
|
0aaf6e0995 | ||
|
|
d1e88a52ac | ||
|
|
9cb8443f94 | ||
|
|
9d5c620167 | ||
|
|
c212ebffcd | ||
|
|
35c4147379 | ||
|
|
e52540eec3 | ||
|
|
b26e663960 | ||
|
|
f4ffcf5fbc | ||
|
|
914495ab97 | ||
|
|
1cad19d9c6 | ||
|
|
e674b9afe6 |
@@ -201,6 +201,150 @@ if (response.IsFailure)
|
||||
Console.WriteLine("Model Id: {0}", response.Value.Id);
|
||||
```
|
||||
|
||||
### Files API
|
||||
|
||||
The `AnthropicApiClient` provides support for the Anthropic Files API, which allows you to upload and manage files for use with the Anthropic API.
|
||||
|
||||
> [!NOTE]
|
||||
> The Files API is currently in beta. To use the Files API, you’ll need to include the beta feature header: `anthropic-beta: files-api-2025-04-14`
|
||||
|
||||
#### Create a File
|
||||
|
||||
You can create a file using the Files API in several ways:
|
||||
|
||||
##### From a Byte Array
|
||||
|
||||
```csharp
|
||||
var fileBytes = await File.ReadAllBytesAsync("path/to/file.txt");
|
||||
var request = new CreateFileRequest(fileBytes, "file.txt", "text/plain");
|
||||
var result = await client.CreateFileAsync(request);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var file = result.Value;
|
||||
Console.WriteLine($"Created file: {file.Name} (ID: {file.Id})");
|
||||
}
|
||||
```
|
||||
|
||||
##### From a Stream
|
||||
|
||||
```csharp
|
||||
using var fileStream = File.OpenRead("path/to/file.txt");
|
||||
var request = new CreateFileRequest(fileStream, "file.txt", "text/plain");
|
||||
|
||||
var result = await client.CreateFileAsync(request);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var file = result.Value;
|
||||
Console.WriteLine($"Created file: {file.Name} (ID: {file.Id})");
|
||||
}
|
||||
```
|
||||
|
||||
#### List Files
|
||||
|
||||
You can list files in your account using pagination:
|
||||
|
||||
##### Single Page
|
||||
|
||||
```csharp
|
||||
var result = await client.ListFilesAsync();
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var page = result.Value;
|
||||
Console.WriteLine($"Found {page.Data.Count} files");
|
||||
|
||||
foreach (var file in page.Data)
|
||||
{
|
||||
Console.WriteLine($"- {file.Name} (ID: {file.Id}, Size: {file.Size} bytes)");
|
||||
}
|
||||
|
||||
if (page.HasMore)
|
||||
{
|
||||
Console.WriteLine("More files available...");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### With Pagination Options
|
||||
|
||||
```csharp
|
||||
var pagingRequest = new PagingRequest(afterId: "file_12345", limit: 10);
|
||||
var result = await client.ListFilesAsync(pagingRequest);
|
||||
```
|
||||
|
||||
##### All Files (Multiple Pages)
|
||||
|
||||
```csharp
|
||||
await foreach (var pageResult in client.ListAllFilesAsync(limit: 20))
|
||||
{
|
||||
if (pageResult.IsSuccess)
|
||||
{
|
||||
var page = pageResult.Value;
|
||||
foreach (var file in page.Data)
|
||||
{
|
||||
Console.WriteLine($"- {file.Name} (ID: {file.Id})");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Get File Information
|
||||
|
||||
Retrieve metadata about a specific file:
|
||||
|
||||
```csharp
|
||||
var result = await client.GetFileInfoAsync("file_12345");
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var file = result.Value;
|
||||
Console.WriteLine($"File: {file.Name}");
|
||||
Console.WriteLine($"ID: {file.Id}");
|
||||
Console.WriteLine($"MIME Type: {file.MimeType}");
|
||||
Console.WriteLine($"Size: {file.Size} bytes");
|
||||
Console.WriteLine($"Created: {file.CreatedAt}");
|
||||
Console.WriteLine($"Downloadable: {file.Downloadable}");
|
||||
}
|
||||
```
|
||||
|
||||
#### Get File Content
|
||||
|
||||
Download the content of a file as a stream:
|
||||
|
||||
```csharp
|
||||
var result = await client.GetFileAsync("file_12345");
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
using var contentStream = result.Value;
|
||||
using var reader = new StreamReader(contentStream);
|
||||
var content = await reader.ReadToEndAsync();
|
||||
|
||||
Console.WriteLine("File content:");
|
||||
Console.WriteLine(content);
|
||||
}
|
||||
```
|
||||
|
||||
#### Delete a File
|
||||
|
||||
Remove a file from your account:
|
||||
|
||||
```csharp
|
||||
var result = await client.DeleteFileAsync("file_12345");
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var deleteResponse = result.Value;
|
||||
Console.WriteLine($"Deleted file: {deleteResponse.Id}");
|
||||
Console.WriteLine($"Type: {deleteResponse.Type}");
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The Files API has certain limitations on file size, supported file types, and usage quotas. Please refer to the [Anthropic API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/files) for the most up-to-date information on these limitations.
|
||||
|
||||
### Create a message
|
||||
|
||||
The `AnthropicApiClient` exposes a method named `CreateMessageAsync` that can be used to create a message. The method requires a `MessageRequest` or a `StreamMessageRequest` instance as a parameter. The `MessageRequest` class is used to create a message whose response is not streamed and the `StreamMessageRequest` class is used to create a message whose response is streamed. The `MessageRequest` instance's properties can be set to configure how the message is created.
|
||||
|
||||
@@ -18,6 +18,7 @@ public class AnthropicApiClient : IAnthropicApiClient
|
||||
private string CountTokensEndpoint => $"{MessagesEndpoint}/count_tokens";
|
||||
private string MessageBatchesEndpoint => $"{MessagesEndpoint}/batches";
|
||||
private const string ModelsEndpoint = "models";
|
||||
private const string FilesEndpoint = "files";
|
||||
private const string JsonContentType = "application/json";
|
||||
private const string EventPrefix = "event:";
|
||||
private const string DataPrefix = "data:";
|
||||
@@ -380,6 +381,64 @@ public class AnthropicApiClient : IAnthropicApiClient
|
||||
return await CreateResultAsync<AnthropicModel>(response);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<AnthropicResult<AnthropicFile>> CreateFileAsync(CreateFileRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await SendFileRequestAsync(FilesEndpoint, request, cancellationToken);
|
||||
return await CreateResultAsync<AnthropicFile>(response);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<AnthropicResult<Page<AnthropicFile>>> ListFilesAsync(PagingRequest? request = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pagingRequest = request ?? new PagingRequest();
|
||||
var endpoint = $"{FilesEndpoint}?{pagingRequest.ToQueryParameters()}";
|
||||
var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken);
|
||||
return await CreateResultAsync<Page<AnthropicFile>>(response);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async IAsyncEnumerable<AnthropicResult<Page<AnthropicFile>>> ListAllFilesAsync(int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await foreach (var result in GetAllPagesAsync<AnthropicFile>(FilesEndpoint, limit, cancellationToken))
|
||||
{
|
||||
yield return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<AnthropicResult<AnthropicFile>> GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var endpoint = $"{FilesEndpoint}/{fileId}";
|
||||
var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken);
|
||||
return await CreateResultAsync<AnthropicFile>(response);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<AnthropicResult<Stream>> GetFileAsync(string fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var endpoint = $"{FilesEndpoint}/{fileId}/content";
|
||||
var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken);
|
||||
|
||||
if (response.IsSuccessStatusCode is false)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var error = Deserialize<AnthropicError>(content) ?? new AnthropicError();
|
||||
return AnthropicResult<Stream>.Failure(error, new AnthropicHeaders(response.Headers));
|
||||
}
|
||||
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
return AnthropicResult<Stream>.Success(stream, new AnthropicHeaders(response.Headers));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<AnthropicResult<AnthropicFileDeleteResponse>> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var endpoint = $"{FilesEndpoint}/{fileId}";
|
||||
var response = await SendRequestAsync(endpoint, HttpMethod.Delete, cancellationToken);
|
||||
return await CreateResultAsync<AnthropicFileDeleteResponse>(response);
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<AnthropicResult<Page<T>>> GetAllPagesAsync<T>(string endpoint, int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pagingRequest = new PagingRequest(limit: limit);
|
||||
@@ -462,6 +521,17 @@ public class AnthropicApiClient : IAnthropicApiClient
|
||||
return await _httpClient.PostAsync(endpoint, requestContent, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> SendFileRequestAsync(string endpoint, CreateFileRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var multipartContent = new MultipartFormDataContent();
|
||||
|
||||
using var fileContent = new ByteArrayContent(request.File);
|
||||
fileContent.Headers.ContentType = new MediaTypeHeaderValue(request.FileType);
|
||||
multipartContent.Add(fileContent, "file", request.FileName);
|
||||
|
||||
return await _httpClient.PostAsync(endpoint, multipartContent, cancellationToken);
|
||||
}
|
||||
|
||||
private string Serialize<T>(T obj) => JsonSerializer.Serialize(obj, JsonSerializationOptions.DefaultOptions);
|
||||
private T? Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json, JsonSerializationOptions.DefaultOptions);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<PackageId>AnthropicClient</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Version>1.1.0</Version>
|
||||
<Authors>Stevan Freeborn</Authors>
|
||||
<Description>Anthropic Client Library</Description>
|
||||
<PackageProjectUrl>https://anthropicclient.stevanfreeborn.com/</PackageProjectUrl>
|
||||
|
||||
@@ -2,6 +2,23 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [versionize](https://github.com/versionize/versionize) for commit guidelines.
|
||||
|
||||
<a name="1.1.0"></a>
|
||||
## [1.1.0](https://www.github.com/StevanFreeborn/anthropic-client/releases/tag/v1.1.0) (2025-07-15)
|
||||
|
||||
### Features
|
||||
|
||||
* add missing model constants ([b859859](https://www.github.com/StevanFreeborn/anthropic-client/commit/b8598595484739027bdd83cd8cdfe122ad3cb9cf))
|
||||
* add support for file source and url source ([1f83899](https://www.github.com/StevanFreeborn/anthropic-client/commit/1f83899eb88b9ee9d87d246158defd7cbde9b01e))
|
||||
* implement `GetFileInfoAsync`, `GetFileAsync`, and `DeleteFileAsync` ([d1e88a5](https://www.github.com/StevanFreeborn/anthropic-client/commit/d1e88a52ace603a5c017cca3bf48953846d7b271))
|
||||
* implement list all files method ([9cb8443](https://www.github.com/StevanFreeborn/anthropic-client/commit/9cb8443f94d2086192700aeb1f2dc977a86cb752))
|
||||
* implement listing a page of files ([9d5c620](https://www.github.com/StevanFreeborn/anthropic-client/commit/9d5c6201674bfb0923a0b0d70530286980ebdf62))
|
||||
* initial implementation of creating a file via the Files API ([e674b9a](https://www.github.com/StevanFreeborn/anthropic-client/commit/e674b9afe693143841a168c8356ef93079445db0))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove unnecessary usings ([1cad19d](https://www.github.com/StevanFreeborn/anthropic-client/commit/1cad19d9c6c4418fef3c206f700ca14b7d250e26))
|
||||
* use sync copy to method ([b48baff](https://www.github.com/StevanFreeborn/anthropic-client/commit/b48baffb60a8acc8b89a60e53589bc04362a9505))
|
||||
|
||||
<a name="1.0.0"></a>
|
||||
## [1.0.0](https://www.github.com/StevanFreeborn/anthropic-client/releases/tag/v1.0.0) (2025-07-09)
|
||||
|
||||
|
||||
@@ -111,4 +111,53 @@ public interface IAnthropicApiClient
|
||||
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicModel"/>.</returns>
|
||||
Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a file asynchronously using the Files API.
|
||||
/// </summary>
|
||||
/// <param name="request">The file creation request.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicFile"/>.</returns>
|
||||
Task<AnthropicResult<AnthropicFile>> CreateFileAsync(CreateFileRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Lists files asynchronously, returning a single page of results.
|
||||
/// </summary>
|
||||
/// <param name="request">The paging request to use for listing the files.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="AnthropicFile"/>.</returns>
|
||||
Task<AnthropicResult<Page<AnthropicFile>>> ListFilesAsync(PagingRequest? request = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Lists all files asynchronously, returning every page of results.
|
||||
/// </summary>
|
||||
/// <param name="limit">The maximum number of files to return in each page.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
|
||||
/// <returns>An asynchronous enumerable that yields the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="AnthropicFile"/>.</returns>
|
||||
IAsyncEnumerable<AnthropicResult<Page<AnthropicFile>>> ListAllFilesAsync(int limit = 20, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a file's metadata by its ID asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="fileId">The ID of the file to get.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicFile"/>.</returns>
|
||||
Task<AnthropicResult<AnthropicFile>> GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a file's content by its ID asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="fileId">The ID of the file to get the content for.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is a stream containing the file content.</returns>
|
||||
Task<AnthropicResult<Stream>> GetFileAsync(string fileId, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a file by its ID asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="fileId">The ID of the file to delete.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicFileDeleteResponse"/>.</returns>
|
||||
Task<AnthropicResult<AnthropicFileDeleteResponse>> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -16,6 +16,8 @@ class SourceConverter : JsonConverter<Source>
|
||||
{
|
||||
SourceType.Text => JsonSerializer.Deserialize<TextSource>(root.GetRawText(), options)!,
|
||||
SourceType.Content => JsonSerializer.Deserialize<CustomSource>(root.GetRawText(), options)!,
|
||||
SourceType.File => JsonSerializer.Deserialize<FileSource>(root.GetRawText(), options)!,
|
||||
SourceType.Url => JsonSerializer.Deserialize<UrlSource>(root.GetRawText(), options)!,
|
||||
SourceType.Base64 => DeserializeBase64Source(root, options),
|
||||
_ => throw new JsonException($"Unknown source type: {type}")
|
||||
};
|
||||
@@ -54,6 +56,18 @@ class SourceConverter : JsonConverter<Source>
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is FileSource fileSource)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, fileSource, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is UrlSource urlSource)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, urlSource, options);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonSerializer.Serialize(writer, value, value.GetType(), options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AnthropicClient.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a file object from the Anthropic Files API.
|
||||
/// </summary>
|
||||
public class AnthropicFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique object identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Object type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Original filename of the uploaded file.
|
||||
/// </summary>
|
||||
[JsonPropertyName("filename")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Date file was created.
|
||||
/// </summary>
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the file in bytes.
|
||||
/// </summary>
|
||||
[JsonPropertyName("size_bytes")]
|
||||
public long Size { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// MIME type of the file.
|
||||
/// </summary>
|
||||
[JsonPropertyName("mime_type")]
|
||||
public string MimeType { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the file can be downloaded.
|
||||
/// </summary>
|
||||
[JsonPropertyName("downloadable")]
|
||||
public bool Downloadable { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace AnthropicClient.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response from deleting a file in the Anthropic API.
|
||||
/// </summary>
|
||||
public class AnthropicFileDeleteResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the file that was deleted.
|
||||
/// </summary>
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the response type
|
||||
/// </summary>
|
||||
public string Type { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -20,6 +20,16 @@ public static class AnthropicModels
|
||||
/// </summary>
|
||||
public const string Claude3OpusLatest = "claude-3-opus-latest";
|
||||
|
||||
/// <summary>
|
||||
/// The Claude 4 Opus model.
|
||||
/// </summary>
|
||||
public const string ClaudeOpus420250514 = "claude-opus-4-20250514";
|
||||
|
||||
/// <summary>
|
||||
/// The Claude 4 Opus model.
|
||||
/// </summary>
|
||||
public const string ClaudeOpus40 = "claude-opus-4-0";
|
||||
|
||||
/// <summary>
|
||||
/// The Claude 3 Sonnet model.
|
||||
/// </summary>
|
||||
@@ -50,6 +60,26 @@ public static class AnthropicModels
|
||||
/// </summary>
|
||||
public const string Claude35SonnetLatest = "claude-3-5-sonnet-latest";
|
||||
|
||||
/// <summary>
|
||||
/// The Claude 3 Sonnet model
|
||||
/// </summary>
|
||||
public const string Claude37Sonnet20250219 = "claude-3-7-sonnet-20250219";
|
||||
|
||||
/// <summary>
|
||||
/// The Claude 3 Sonnet model
|
||||
/// </summary>
|
||||
public const string Claude37SonnetLatest = "claude-3-7-sonnet-latest";
|
||||
|
||||
/// <summary>
|
||||
/// The Claude 4 Sonnet model.
|
||||
/// </summary>
|
||||
public const string ClaudeSonnet420250514 = "claude-sonnet-4-20250514";
|
||||
|
||||
/// <summary>
|
||||
/// The Claude 4 Sonnet model.
|
||||
/// </summary>
|
||||
public const string ClaudeSonnet40 = "claude-sonnet-4-0";
|
||||
|
||||
/// <summary>
|
||||
/// The Claude 3 Haiku model.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using AnthropicClient.Utils;
|
||||
|
||||
namespace AnthropicClient.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to create a file via the Anthropic Files API.
|
||||
/// </summary>
|
||||
public class CreateFileRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The file content as a byte array.
|
||||
/// </summary>
|
||||
public byte[] File { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The original filename of the file being uploaded.
|
||||
/// </summary>
|
||||
public string FileName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The MIME type of the file.
|
||||
/// </summary>
|
||||
public string FileType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CreateFileRequest"/> class.
|
||||
/// </summary>
|
||||
/// <param name="file">The file content as a byte array.</param>
|
||||
/// <param name="fileName">The original filename of the file being uploaded.</param>
|
||||
/// <param name="fileType">The MIME type of the file.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="file"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="fileName"/> or <paramref name="fileType"/> is null or whitespace.</exception>
|
||||
public CreateFileRequest(byte[] file, string fileName, string fileType)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(file, nameof(file));
|
||||
ArgumentValidator.ThrowIfNullOrWhitespace(fileName, nameof(fileName));
|
||||
ArgumentValidator.ThrowIfNullOrWhitespace(fileType, nameof(fileType));
|
||||
|
||||
File = file;
|
||||
FileName = fileName;
|
||||
FileType = fileType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CreateFileRequest"/> class from a stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream containing the file content.</param>
|
||||
/// <param name="fileName">The original filename of the file being uploaded.</param>
|
||||
/// <param name="fileType">The MIME type of the file.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="fileName"/> or <paramref name="fileType"/> is null or whitespace.</exception>
|
||||
public CreateFileRequest(Stream stream, string fileName, string fileType)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(stream, nameof(stream));
|
||||
ArgumentValidator.ThrowIfNullOrWhitespace(fileName, nameof(fileName));
|
||||
ArgumentValidator.ThrowIfNullOrWhitespace(fileType, nameof(fileType));
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
stream.CopyTo(memoryStream);
|
||||
var fileContent = memoryStream.ToArray();
|
||||
|
||||
File = fileContent;
|
||||
FileName = fileName;
|
||||
FileType = fileType;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AnthropicClient.Utils;
|
||||
|
||||
namespace AnthropicClient.Models;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AnthropicClient.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a file source in the Anthropic API.
|
||||
/// </summary>
|
||||
public class FileSource : Source
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for the file source.
|
||||
/// </summary>
|
||||
[JsonPropertyName("file_id")]
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileSource"/> class.
|
||||
/// </summary>
|
||||
/// <returns>A new instance of <see cref="FileSource"/> with the type set to "file".</returns>
|
||||
public FileSource() : base(SourceType.File)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileSource"/> class with a specified file ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier for the file source.</param>
|
||||
/// <returns>A new instance of <see cref="FileSource"/>.</returns>
|
||||
public FileSource(string id) : base(SourceType.File)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
@@ -53,4 +53,32 @@ public class ImageContent : Content
|
||||
|
||||
Source = new ImageSource(mediaType, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageContent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="source">The source of the image.</param>
|
||||
/// <returns>A new instance of the <see cref="ImageContent"/> class.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when the source is null.</exception>
|
||||
public ImageContent(Source source) : base(ContentType.Image)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(source, nameof(source));
|
||||
|
||||
Source = source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageContent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="source">The source of the image.</param>
|
||||
/// <param name="cacheControl">The cache control to be used for the content.</param>
|
||||
/// <returns>A new instance of the <see cref="ImageContent"/> class.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when the source or cache control is null.</exception>
|
||||
public ImageContent(Source source, CacheControl cacheControl) : base(ContentType.Image, cacheControl)
|
||||
{
|
||||
ArgumentValidator.ThrowIfNull(source, nameof(source));
|
||||
ArgumentValidator.ThrowIfNull(cacheControl, nameof(cacheControl));
|
||||
|
||||
Source = source;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AnthropicClient.Utils;
|
||||
|
||||
namespace AnthropicClient.Models;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -19,4 +19,14 @@ public static class SourceType
|
||||
/// The text document source type.
|
||||
/// </summary>
|
||||
public const string Text = "text";
|
||||
|
||||
/// <summary>
|
||||
/// The file document source type.
|
||||
/// </summary>
|
||||
public const string File = "file";
|
||||
|
||||
/// <summary>
|
||||
/// The URL document source type.
|
||||
/// </summary>
|
||||
public const string Url = "url";
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace AnthropicClient.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a URL source in the Anthropic API.
|
||||
/// </summary>
|
||||
public class UrlSource : Source
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the URL of the source document.
|
||||
/// </summary>
|
||||
public string Url { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UrlSource"/> class.
|
||||
/// </summary>
|
||||
/// <returns>A new instance of <see cref="UrlSource"/> with the type set to "url".</returns>
|
||||
public UrlSource() : base(SourceType.Url)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UrlSource"/> class with a specified URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the source document.</param>
|
||||
/// <returns>A new instance of <see cref="UrlSource"/>.</returns>
|
||||
public UrlSource(string url) : base(SourceType.Url)
|
||||
{
|
||||
Url = url;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,15 @@ using AnthropicClient.Tests.Files;
|
||||
|
||||
namespace AnthropicClient.Tests.EndToEnd;
|
||||
|
||||
public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture)
|
||||
public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture), IAsyncLifetime
|
||||
{
|
||||
private readonly List<string> _filesToDelete = [];
|
||||
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse()
|
||||
{
|
||||
@@ -96,11 +103,41 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
text.Should().Contain("elephant");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenImageIsSentAsUrl_ItShouldReturnResponse()
|
||||
{
|
||||
var request = new MessageRequest(
|
||||
model: AnthropicModels.Claude3Haiku,
|
||||
messages: [
|
||||
new(MessageRole.User, [
|
||||
new ImageContent(new UrlSource("https://ftp.stevanfreeborn.com/share/anthropic-client/ant.jpg")),
|
||||
new TextContent("What is in this image?")
|
||||
]),
|
||||
]
|
||||
);
|
||||
|
||||
var result = await _client.CreateMessageAsync(request);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeOfType<MessageResponse>();
|
||||
result.Value.Content.Should().NotBeNullOrEmpty();
|
||||
|
||||
var text = result.Value.Content.Aggregate("", static (acc, content) =>
|
||||
{
|
||||
if (content is TextContent textContent)
|
||||
{
|
||||
acc += textContent.Text;
|
||||
}
|
||||
|
||||
return acc;
|
||||
});
|
||||
|
||||
text.Should().Contain("ant");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenSystemMessagesContainCacheControl_ItShouldUseCache()
|
||||
{
|
||||
var client = CreateClient(new HttpClient());
|
||||
|
||||
var storyPath = TestFileHelper.GetTestFilePath("story.txt");
|
||||
var storyText = await File.ReadAllTextAsync(storyPath);
|
||||
|
||||
@@ -127,7 +164,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
||||
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")]));
|
||||
|
||||
var resultTwo = await client.CreateMessageAsync(request);
|
||||
var resultTwo = await _client.CreateMessageAsync(request);
|
||||
|
||||
resultTwo.IsSuccess.Should().BeTrue();
|
||||
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
||||
@@ -138,8 +175,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenMessagesContainCacheControl_ItShouldUseCache()
|
||||
{
|
||||
var client = CreateClient(new HttpClient());
|
||||
|
||||
var storyPath = TestFileHelper.GetTestFilePath("story.txt");
|
||||
var storyText = await File.ReadAllTextAsync(storyPath);
|
||||
|
||||
@@ -153,7 +188,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
]
|
||||
);
|
||||
|
||||
var resultOne = await client.CreateMessageAsync(request);
|
||||
var resultOne = await _client.CreateMessageAsync(request);
|
||||
|
||||
resultOne.IsSuccess.Should().BeTrue();
|
||||
resultOne.Value.Should().BeOfType<MessageResponse>();
|
||||
@@ -163,7 +198,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
||||
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")]));
|
||||
|
||||
var resultTwo = await client.CreateMessageAsync(request);
|
||||
var resultTwo = await _client.CreateMessageAsync(request);
|
||||
|
||||
resultTwo.IsSuccess.Should().BeTrue();
|
||||
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
||||
@@ -174,8 +209,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenToolsContainCacheControl_ItShouldUseCache()
|
||||
{
|
||||
var client = CreateClient(new HttpClient());
|
||||
|
||||
var func = (string ticker) => ticker;
|
||||
|
||||
var tools = Enumerable
|
||||
@@ -195,7 +228,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
tools: tools
|
||||
);
|
||||
|
||||
var resultOne = await client.CreateMessageAsync(request);
|
||||
var resultOne = await _client.CreateMessageAsync(request);
|
||||
|
||||
resultOne.IsSuccess.Should().BeTrue();
|
||||
resultOne.Value.Should().BeOfType<MessageResponse>();
|
||||
@@ -205,7 +238,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
||||
request.Messages.Add(new(MessageRole.User, [new TextContent("Could you tell me the stock price for AAPL?")]));
|
||||
|
||||
var resultTwo = await client.CreateMessageAsync(request);
|
||||
var resultTwo = await _client.CreateMessageAsync(request);
|
||||
|
||||
resultTwo.IsSuccess.Should().BeTrue();
|
||||
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
||||
@@ -228,9 +261,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
]
|
||||
);
|
||||
|
||||
var client = CreateClient(new HttpClient());
|
||||
|
||||
var result = await client.CreateMessageAsync(request);
|
||||
var result = await _client.CreateMessageAsync(request);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeOfType<MessageResponse>();
|
||||
@@ -256,8 +287,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
var bytes = await File.ReadAllBytesAsync(pdfPath);
|
||||
var base64Data = Convert.ToBase64String(bytes);
|
||||
|
||||
var client = CreateClient(new HttpClient());
|
||||
|
||||
var request = new MessageRequest(
|
||||
model: AnthropicModels.Claude35Sonnet,
|
||||
messages: [
|
||||
@@ -268,7 +297,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
]
|
||||
);
|
||||
|
||||
var resultOne = await client.CreateMessageAsync(request);
|
||||
var resultOne = await _client.CreateMessageAsync(request);
|
||||
|
||||
resultOne.IsSuccess.Should().BeTrue();
|
||||
resultOne.Value.Should().BeOfType<MessageResponse>();
|
||||
@@ -278,7 +307,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
||||
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this paper?")]));
|
||||
|
||||
var resultTwo = await client.CreateMessageAsync(request);
|
||||
var resultTwo = await _client.CreateMessageAsync(request);
|
||||
|
||||
resultTwo.IsSuccess.Should().BeTrue();
|
||||
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
||||
@@ -395,6 +424,58 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenCitationsAreEnabledForFileSource_ItShouldReturnCitationsInResponse()
|
||||
{
|
||||
var fileName = "story.txt";
|
||||
var fileType = "text/plain";
|
||||
var filePath = TestFileHelper.GetTestFilePath("story.txt");
|
||||
var fileContent = await File.ReadAllBytesAsync(filePath);
|
||||
var createFileRequest = new CreateFileRequest(fileContent, fileName, fileType);
|
||||
|
||||
var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||
var client = CreateClient(httpClient);
|
||||
|
||||
var createdFile = await client.CreateFileAsync(createFileRequest);
|
||||
|
||||
var request = new MessageRequest(
|
||||
model: AnthropicModels.Claude35HaikuLatest,
|
||||
messages: [
|
||||
new(
|
||||
MessageRole.User,
|
||||
[
|
||||
new DocumentContent(new FileSource(createdFile.Value.Id))
|
||||
{
|
||||
Title = "A Story",
|
||||
Context = "This is a trustworthy document.",
|
||||
Citations = new() { Enabled = true }
|
||||
},
|
||||
new TextContent("Can you tell me what the title of this story is?"),
|
||||
]
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
var result = await client.CreateMessageAsync(request);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
var textContents = result.Value.Content.OfType<TextContent>();
|
||||
|
||||
var messageContent = textContents.Aggregate(new StringBuilder(), (sb, content) =>
|
||||
{
|
||||
sb.Append(content.Text);
|
||||
return sb;
|
||||
});
|
||||
messageContent.ToString().Should().MatchRegex("The Forgotten Lighthouse");
|
||||
|
||||
var citations = textContents.SelectMany(static c => c.Citations is null ? [] : c.Citations);
|
||||
citations.OfType<CharacterLocationCitation>().Should().NotBeEmpty();
|
||||
|
||||
_filesToDelete.Add(createdFile.Value.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForTextDocumentSource_ItShouldReturnCitationsInResponse()
|
||||
{
|
||||
@@ -516,6 +597,64 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForFileSource_ItShouldReturnCitationsInResponse()
|
||||
{
|
||||
var fileName = "story.txt";
|
||||
var fileType = "text/plain";
|
||||
var filePath = TestFileHelper.GetTestFilePath("story.txt");
|
||||
var fileContent = await File.ReadAllBytesAsync(filePath);
|
||||
var createFileRequest = new CreateFileRequest(fileContent, fileName, fileType);
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||
var client = CreateClient(httpClient);
|
||||
|
||||
var createdFile = await client.CreateFileAsync(createFileRequest);
|
||||
|
||||
var request = new StreamMessageRequest(
|
||||
model: AnthropicModels.Claude35HaikuLatest,
|
||||
messages: [
|
||||
new(
|
||||
MessageRole.User,
|
||||
[
|
||||
new DocumentContent(new FileSource(createdFile.Value.Id))
|
||||
{
|
||||
Title = "A Story",
|
||||
Context = "This is a trustworthy document.",
|
||||
Citations = new() { Enabled = true }
|
||||
},
|
||||
new TextContent("Can you tell me what the title of this story is?"),
|
||||
]
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
var result = client.CreateMessageAsync(request);
|
||||
|
||||
var messageCompleteEvent = await result
|
||||
.Where(e => e.Type is EventType.MessageComplete)
|
||||
.FirstAsync();
|
||||
|
||||
var textContents = messageCompleteEvent.Data
|
||||
.As<MessageCompleteEventData>()
|
||||
.Message
|
||||
.Content
|
||||
.OfType<TextContent>();
|
||||
|
||||
var messageContent = textContents.Aggregate(new StringBuilder(), (sb, content) =>
|
||||
{
|
||||
sb.Append(content.Text);
|
||||
return sb;
|
||||
});
|
||||
messageContent.ToString().Should().MatchRegex("The Forgotten Lighthouse");
|
||||
|
||||
var citations = textContents.SelectMany(static c => c.Citations is null ? [] : c.Citations);
|
||||
citations.OfType<CharacterLocationCitation>().Should().NotBeEmpty();
|
||||
|
||||
_filesToDelete.Add(createdFile.Value.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CountMessageTokensAsync_WhenCalled_ItShouldReturnResponse()
|
||||
{
|
||||
@@ -687,4 +826,120 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
||||
result.Value.Id.Should().Be(createResult.Value.Id);
|
||||
result.Value.ProcessingStatus.Should().Be(MessageBatchStatus.Canceling);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateFileAsync_WhenCalled_ItShouldReturnResponse()
|
||||
{
|
||||
var fileName = "story.txt";
|
||||
var fileType = "text/plain";
|
||||
var filePath = TestFileHelper.GetTestFilePath("story.txt");
|
||||
var fileContent = await File.ReadAllBytesAsync(filePath);
|
||||
var request = new CreateFileRequest(fileContent, fileName, fileType);
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||
var client = CreateClient(httpClient);
|
||||
|
||||
var result = await client.CreateFileAsync(request);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeOfType<AnthropicFile>();
|
||||
result.Value.Name.Should().Be(fileName);
|
||||
result.Value.MimeType.Should().Be(fileType);
|
||||
|
||||
_filesToDelete.Add(result.Value.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListFilesAsync_WhenCalled_ItShouldReturnPageOfFiles()
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||
var client = CreateClient(httpClient);
|
||||
|
||||
var fileBytes = await File.ReadAllBytesAsync(TestFileHelper.GetTestFilePath("story.txt"));
|
||||
var createFileRequest = new CreateFileRequest(fileBytes, "story.txt", "text/plain");
|
||||
var createdFile = await client.CreateFileAsync(createFileRequest);
|
||||
|
||||
var result = await client.ListFilesAsync();
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeOfType<Page<AnthropicFile>>();
|
||||
result.Value.Data.Should().ContainSingle(f => f.Id == createdFile.Value.Id);
|
||||
|
||||
_filesToDelete.Add(createdFile.Value.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListAllFilesAsync_WhenCalled_ItShouldReturnAllFiles()
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||
var client = CreateClient(httpClient);
|
||||
|
||||
var fileBytes = await File.ReadAllBytesAsync(TestFileHelper.GetTestFilePath("story.txt"));
|
||||
var createFileRequest = new CreateFileRequest(fileBytes, "story.txt", "text/plain");
|
||||
var createdFile = await client.CreateFileAsync(createFileRequest);
|
||||
|
||||
var responses = await client.ListAllFilesAsync(limit: 1).ToListAsync();
|
||||
|
||||
responses.Should().HaveCountGreaterThan(0);
|
||||
responses.Select(r => r.Value)
|
||||
.SelectMany(p => p.Data)
|
||||
.Should()
|
||||
.ContainSingle(f => f.Id == createdFile.Value.Id);
|
||||
|
||||
_filesToDelete.Add(createdFile.Value.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFileInfoAsync_WhenCalled_ItShouldReturnFile()
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||
var client = CreateClient(httpClient);
|
||||
|
||||
var fileBytes = await File.ReadAllBytesAsync(TestFileHelper.GetTestFilePath("story.txt"));
|
||||
var createFileRequest = new CreateFileRequest(fileBytes, "story.txt", "text/plain");
|
||||
var createdFile = await client.CreateFileAsync(createFileRequest);
|
||||
|
||||
var result = await client.GetFileInfoAsync(createdFile.Value.Id);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeOfType<AnthropicFile>();
|
||||
result.Value.Id.Should().Be(createdFile.Value.Id);
|
||||
|
||||
_filesToDelete.Add(createdFile.Value.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteFileAsync_WhenCalled_ItShouldReturnDeleteResponse()
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||
var client = CreateClient(httpClient);
|
||||
|
||||
var fileBytes = await File.ReadAllBytesAsync(TestFileHelper.GetTestFilePath("story.txt"));
|
||||
var createFileRequest = new CreateFileRequest(fileBytes, "story.txt", "text/plain");
|
||||
var createdFile = await client.CreateFileAsync(createFileRequest);
|
||||
|
||||
var result = await client.DeleteFileAsync(createdFile.Value.Id);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeOfType<AnthropicFileDeleteResponse>();
|
||||
result.Value.Id.Should().Be(createdFile.Value.Id);
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||
var client = CreateClient(httpClient);
|
||||
|
||||
foreach (var file in _filesToDelete)
|
||||
{
|
||||
var result = await client.DeleteFileAsync(file);
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1928,4 +1928,476 @@ public class AnthropicApiClientTests : IntegrationTest
|
||||
result.Value.Id.Should().Be(batchId);
|
||||
result.Value.Type.Should().Be("message_batch_deleted");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateFileAsync_WhenCalled_ItShouldReturnFile()
|
||||
{
|
||||
_mockHttpMessageHandler
|
||||
.WhenCreateFileRequest()
|
||||
.Respond(
|
||||
HttpStatusCode.OK,
|
||||
"application/json",
|
||||
@"{
|
||||
""created_at"": ""2023-11-07T05:31:56Z"",
|
||||
""downloadable"": false,
|
||||
""filename"": ""example.txt"",
|
||||
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
|
||||
""mime_type"": ""text/plain"",
|
||||
""size_bytes"": 1234,
|
||||
""type"": ""file""
|
||||
}"
|
||||
);
|
||||
|
||||
var fileContent = new MemoryStream(Encoding.UTF8.GetBytes("Example file content"));
|
||||
var request = new CreateFileRequest(fileContent, "example.txt", "text/plain");
|
||||
|
||||
var result = await Client.CreateFileAsync(request);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeEquivalentTo(new AnthropicFile()
|
||||
{
|
||||
CreatedAt = DateTimeOffset.Parse("2023-11-07T05:31:56Z"),
|
||||
Downloadable = false,
|
||||
Name = "example.txt",
|
||||
Id = "file_013Zva2CMHLNnXjNJJKqJ2EF",
|
||||
MimeType = "text/plain",
|
||||
Size = 1234,
|
||||
Type = "file"
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateFileAsync_WhenCalledAndRequestFails_ItShouldReturnError()
|
||||
{
|
||||
_mockHttpMessageHandler
|
||||
.WhenCreateFileRequest()
|
||||
.Respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
"application/json",
|
||||
@"{
|
||||
""type"": ""error"",
|
||||
""error"": {
|
||||
""type"": ""invalid_request_error"",
|
||||
""message"": ""file: file size exceeds limit""
|
||||
}
|
||||
}"
|
||||
);
|
||||
|
||||
var fileContent = new MemoryStream(Encoding.UTF8.GetBytes("Example file content"));
|
||||
var request = new CreateFileRequest(fileContent, "example.txt", "text/plain");
|
||||
|
||||
var result = await Client.CreateFileAsync(request);
|
||||
|
||||
result.IsSuccess.Should().BeFalse();
|
||||
result.Error.Should().BeOfType<AnthropicError>();
|
||||
result.Error.Error.Should().BeOfType<InvalidRequestError>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListFilesAsync_WhenCalled_ItShouldReturnPageOfFiles()
|
||||
{
|
||||
_mockHttpMessageHandler
|
||||
.WhenListFilesRequest()
|
||||
.Respond(
|
||||
HttpStatusCode.OK,
|
||||
"application/json",
|
||||
@"{
|
||||
""data"": [
|
||||
{
|
||||
""created_at"": ""2023-11-07T05:31:56Z"",
|
||||
""downloadable"": false,
|
||||
""filename"": ""example.txt"",
|
||||
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
|
||||
""mime_type"": ""text/plain"",
|
||||
""size_bytes"": 1234,
|
||||
""type"": ""file""
|
||||
}
|
||||
],
|
||||
""has_more"": true,
|
||||
""first_id"": ""1"",
|
||||
""last_id"": ""1""
|
||||
}"
|
||||
);
|
||||
|
||||
var result = await Client.ListFilesAsync();
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeOfType<Page<AnthropicFile>>();
|
||||
result.Value.HasMore.Should().BeTrue();
|
||||
result.Value.FirstId.Should().Be("1");
|
||||
result.Value.LastId.Should().Be("1");
|
||||
result.Value.Data.Should().BeEquivalentTo(new AnthropicFile[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
CreatedAt = DateTimeOffset.Parse("2023-11-07T05:31:56Z"),
|
||||
Downloadable = false,
|
||||
Name = "example.txt",
|
||||
Id = "file_013Zva2CMHLNnXjNJJKqJ2EF",
|
||||
MimeType = "text/plain",
|
||||
Size = 1234,
|
||||
Type = "file"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListFilesAsync_WhenCalledAndRequestFails_ItShouldReturnError()
|
||||
{
|
||||
_mockHttpMessageHandler
|
||||
.WhenListFilesRequest()
|
||||
.Respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
"application/json",
|
||||
@"{
|
||||
""type"": ""error"",
|
||||
""error"": {
|
||||
""type"": ""invalid_request_error"",
|
||||
""message"": ""files: file not found""
|
||||
}
|
||||
}"
|
||||
);
|
||||
|
||||
var result = await Client.ListFilesAsync();
|
||||
|
||||
result.IsSuccess.Should().BeFalse();
|
||||
result.Error.Should().BeOfType<AnthropicError>();
|
||||
result.Error.Error.Should().BeOfType<InvalidRequestError>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListFilesAsync_WhenCalledWithPagingRequest_ItShouldReturnPageOfFiles()
|
||||
{
|
||||
var pagingRequest = new PagingRequest(afterId: "next_id", limit: 10);
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.WhenListFilesRequest()
|
||||
.WithQueryString(new Dictionary<string, string>
|
||||
{
|
||||
{ "after_id", pagingRequest.AfterId },
|
||||
{ "limit", pagingRequest.Limit.ToString() },
|
||||
})
|
||||
.Respond(
|
||||
HttpStatusCode.OK,
|
||||
"application/json",
|
||||
@"{
|
||||
""data"": [
|
||||
{
|
||||
""created_at"": ""2023-11-07T05:31:56Z"",
|
||||
""downloadable"": false,
|
||||
""filename"": ""example.txt"",
|
||||
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
|
||||
""mime_type"": ""text/plain"",
|
||||
""size_bytes"": 1234,
|
||||
""type"": ""file""
|
||||
}
|
||||
],
|
||||
""has_more"": true,
|
||||
""first_id"": ""1"",
|
||||
""last_id"": ""1""
|
||||
}"
|
||||
);
|
||||
|
||||
var result = await Client.ListFilesAsync(pagingRequest);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeOfType<Page<AnthropicFile>>();
|
||||
result.Value.HasMore.Should().BeTrue();
|
||||
result.Value.FirstId.Should().Be("1");
|
||||
result.Value.LastId.Should().Be("1");
|
||||
result.Value.Data.Should().BeEquivalentTo(new AnthropicFile[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
CreatedAt = DateTimeOffset.Parse("2023-11-07T05:31:56Z"),
|
||||
Downloadable = false,
|
||||
Name = "example.txt",
|
||||
Id = "file_013Zva2CMHLNnXjNJJKqJ2EF",
|
||||
MimeType = "text/plain",
|
||||
Size = 1234,
|
||||
Type = "file"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListAllFilesAsync_WhenCalled_ItShouldReturnAllFiles()
|
||||
{
|
||||
_mockHttpMessageHandler
|
||||
.WhenListFilesRequest()
|
||||
.WithExactQueryString(new Dictionary<string, string>()
|
||||
{
|
||||
{ "limit", "20" },
|
||||
})
|
||||
.Respond(
|
||||
HttpStatusCode.OK,
|
||||
"application/json",
|
||||
@"{
|
||||
""data"": [
|
||||
{
|
||||
""created_at"": ""2023-11-07T05:31:56Z"",
|
||||
""downloadable"": false,
|
||||
""filename"": ""example.txt"",
|
||||
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
|
||||
""mime_type"": ""text/plain"",
|
||||
""size_bytes"": 1234,
|
||||
""type"": ""file""
|
||||
}
|
||||
],
|
||||
""has_more"": true,
|
||||
""first_id"": ""1"",
|
||||
""last_id"": ""1""
|
||||
}"
|
||||
);
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.WhenListFilesRequest()
|
||||
.WithExactQueryString(new Dictionary<string, string>()
|
||||
{
|
||||
{ "after_id", "1" },
|
||||
{ "limit", "20" },
|
||||
})
|
||||
.Respond(
|
||||
HttpStatusCode.OK,
|
||||
"application/json",
|
||||
@"{
|
||||
""data"": [
|
||||
{
|
||||
""created_at"": ""2023-11-07T05:31:56Z"",
|
||||
""downloadable"": false,
|
||||
""filename"": ""example.txt"",
|
||||
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
|
||||
""mime_type"": ""text/plain"",
|
||||
""size_bytes"": 1234,
|
||||
""type"": ""file""
|
||||
}
|
||||
],
|
||||
""has_more"": false,
|
||||
""first_id"": ""2"",
|
||||
""last_id"": ""2""
|
||||
}"
|
||||
);
|
||||
|
||||
var pageResponses = Client.ListAllFilesAsync();
|
||||
var collectedPages = new List<Page<AnthropicFile>>();
|
||||
|
||||
await foreach (var response in pageResponses)
|
||||
{
|
||||
response.IsSuccess.Should().BeTrue();
|
||||
response.Value.Should().BeOfType<Page<AnthropicFile>>();
|
||||
collectedPages.Add(response.Value);
|
||||
}
|
||||
|
||||
var expectedFile = new AnthropicFile()
|
||||
{
|
||||
CreatedAt = DateTimeOffset.Parse("2023-11-07T05:31:56Z"),
|
||||
Downloadable = false,
|
||||
Name = "example.txt",
|
||||
Id = "file_013Zva2CMHLNnXjNJJKqJ2EF",
|
||||
MimeType = "text/plain",
|
||||
Size = 1234,
|
||||
Type = "file"
|
||||
};
|
||||
|
||||
collectedPages.Should().HaveCount(2);
|
||||
collectedPages.Should().BeEquivalentTo(new List<Page<AnthropicFile>>()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Data = [expectedFile],
|
||||
FirstId = "1",
|
||||
LastId = "1",
|
||||
HasMore = true
|
||||
},
|
||||
new()
|
||||
{
|
||||
Data = [expectedFile],
|
||||
FirstId = "2",
|
||||
LastId = "2",
|
||||
HasMore = false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFileInfoAsync_WhenCalled_ItShouldReturnFile()
|
||||
{
|
||||
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.WhenGetFileRequest(fileId)
|
||||
.Respond(
|
||||
HttpStatusCode.OK,
|
||||
"application/json",
|
||||
@"{
|
||||
""created_at"": ""2023-11-07T05:31:56Z"",
|
||||
""downloadable"": false,
|
||||
""filename"": ""example.txt"",
|
||||
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
|
||||
""mime_type"": ""text/plain"",
|
||||
""size_bytes"": 1234,
|
||||
""type"": ""file""
|
||||
}"
|
||||
);
|
||||
|
||||
var result = await Client.GetFileInfoAsync(fileId);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeEquivalentTo(new AnthropicFile()
|
||||
{
|
||||
CreatedAt = DateTimeOffset.Parse("2023-11-07T05:31:56Z"),
|
||||
Downloadable = false,
|
||||
Name = "example.txt",
|
||||
Id = "file_013Zva2CMHLNnXjNJJKqJ2EF",
|
||||
MimeType = "text/plain",
|
||||
Size = 1234,
|
||||
Type = "file"
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFileInfoAsync_WhenCalledAndRequestFails_ItShouldReturnError()
|
||||
{
|
||||
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.WhenGetFileRequest(fileId)
|
||||
.Respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
"application/json",
|
||||
@"{
|
||||
""type"": ""error"",
|
||||
""error"": {
|
||||
""type"": ""invalid_request_error"",
|
||||
""message"": ""file: file not found""
|
||||
}
|
||||
}"
|
||||
);
|
||||
|
||||
var result = await Client.GetFileInfoAsync(fileId);
|
||||
|
||||
result.IsSuccess.Should().BeFalse();
|
||||
result.Error.Should().BeOfType<AnthropicError>();
|
||||
result.Error.Error.Should().BeOfType<InvalidRequestError>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFileAsync_WhenCalled_ItShouldReturnFileContent()
|
||||
{
|
||||
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
|
||||
var fileContent = new MemoryStream(Encoding.UTF8.GetBytes("Example file content"));
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.WhenGetFileContentRequest(fileId)
|
||||
.Respond(
|
||||
HttpStatusCode.OK,
|
||||
"application/octet-stream",
|
||||
fileContent
|
||||
);
|
||||
|
||||
var result = await Client.GetFileAsync(fileId);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeAssignableTo<Stream>();
|
||||
|
||||
using var streamReader = new StreamReader(result.Value);
|
||||
var content = await streamReader.ReadToEndAsync();
|
||||
content.Should().Be("Example file content");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFileAsync_WhenCalledAndRequestFails_ItShouldReturnError()
|
||||
{
|
||||
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.WhenGetFileContentRequest(fileId)
|
||||
.Respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
"application/json",
|
||||
@"{
|
||||
""type"": ""error"",
|
||||
""error"": {
|
||||
""type"": ""invalid_request_error"",
|
||||
""message"": ""file: file not found""
|
||||
}
|
||||
}"
|
||||
);
|
||||
|
||||
var result = await Client.GetFileAsync(fileId);
|
||||
|
||||
result.IsSuccess.Should().BeFalse();
|
||||
result.Error.Should().BeOfType<AnthropicError>();
|
||||
result.Error.Error.Should().BeOfType<InvalidRequestError>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFileAsync_WhenCalledAndCanNotDeserializeResponse_ItShouldReturnError()
|
||||
{
|
||||
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.WhenGetFileContentRequest(fileId)
|
||||
.Respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
"application/json",
|
||||
@"null"
|
||||
);
|
||||
|
||||
var result = await Client.GetFileAsync(fileId);
|
||||
|
||||
result.IsSuccess.Should().BeFalse();
|
||||
result.Error.Should().BeOfType<AnthropicError>();
|
||||
result.Error.Error.Should().BeOfType<ApiError>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteFileAsync_WhenCalled_ItShouldReturnDeletionResponse()
|
||||
{
|
||||
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.WhenDeleteFileRequest(fileId)
|
||||
.Respond(
|
||||
HttpStatusCode.OK,
|
||||
"application/json",
|
||||
@"{
|
||||
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
|
||||
""type"": ""file_deleted""
|
||||
}"
|
||||
);
|
||||
|
||||
var result = await Client.DeleteFileAsync(fileId);
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeOfType<AnthropicFileDeleteResponse>();
|
||||
result.Value.Id.Should().Be(fileId);
|
||||
result.Value.Type.Should().Be("file_deleted");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteFileAsync_WhenCalledAndRequestFails_ItShouldReturnError()
|
||||
{
|
||||
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
|
||||
|
||||
_mockHttpMessageHandler
|
||||
.WhenDeleteFileRequest(fileId)
|
||||
.Respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
"application/json",
|
||||
@"{
|
||||
""type"": ""error"",
|
||||
""error"": {
|
||||
""type"": ""invalid_request_error"",
|
||||
""message"": ""file: file not found""
|
||||
}
|
||||
}"
|
||||
);
|
||||
|
||||
var result = await Client.DeleteFileAsync(fileId);
|
||||
|
||||
result.IsSuccess.Should().BeFalse();
|
||||
result.Error.Should().BeOfType<AnthropicError>();
|
||||
result.Error.Error.Should().BeOfType<InvalidRequestError>();
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ public static class MockHttpMessageHandlerExtensions
|
||||
private static readonly string CountTokensEndpoint = $"{BaseUrl}/messages/count_tokens";
|
||||
private static readonly string MessageBatchesEndpoint = $"{BaseUrl}/messages/batches";
|
||||
private static readonly string ModelsEndpoint = $"{BaseUrl}/models";
|
||||
private static readonly string FilesEndpoint = $"{BaseUrl}/files";
|
||||
|
||||
private static MockedRequest SetupBaseRequest(
|
||||
this MockHttpMessageHandler mockHttpMessageHandler,
|
||||
@@ -103,4 +104,34 @@ public static class MockHttpMessageHandlerExtensions
|
||||
return mockHttpMessageHandler
|
||||
.SetupBaseRequest(HttpMethod.Delete, $"{MessageBatchesEndpoint}/{batchId}");
|
||||
}
|
||||
|
||||
public static MockedRequest WhenCreateFileRequest(this MockHttpMessageHandler mockHttpMessageHandler)
|
||||
{
|
||||
return mockHttpMessageHandler
|
||||
.SetupBaseRequest(HttpMethod.Post, FilesEndpoint);
|
||||
}
|
||||
|
||||
public static MockedRequest WhenListFilesRequest(this MockHttpMessageHandler mockHttpMessageHandler)
|
||||
{
|
||||
return mockHttpMessageHandler
|
||||
.SetupBaseRequest(HttpMethod.Get, FilesEndpoint);
|
||||
}
|
||||
|
||||
public static MockedRequest WhenGetFileRequest(this MockHttpMessageHandler mockHttpMessageHandler, string fileId)
|
||||
{
|
||||
return mockHttpMessageHandler
|
||||
.SetupBaseRequest(HttpMethod.Get, $"{FilesEndpoint}/{fileId}");
|
||||
}
|
||||
|
||||
public static MockedRequest WhenGetFileContentRequest(this MockHttpMessageHandler mockHttpMessageHandler, string fileId)
|
||||
{
|
||||
return mockHttpMessageHandler
|
||||
.SetupBaseRequest(HttpMethod.Get, $"{FilesEndpoint}/{fileId}/content");
|
||||
}
|
||||
|
||||
public static MockedRequest WhenDeleteFileRequest(this MockHttpMessageHandler mockHttpMessageHandler, string fileId)
|
||||
{
|
||||
return mockHttpMessageHandler
|
||||
.SetupBaseRequest(HttpMethod.Delete, $"{FilesEndpoint}/{fileId}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace AnthropicClient.Tests.Unit.Models;
|
||||
|
||||
public class AnthropicFileDeleteResponseTests : SerializationTest
|
||||
{
|
||||
private readonly string _testJson = @"{
|
||||
""id"": ""file-12345"",
|
||||
""type"": ""file_deleted""
|
||||
}";
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalled_ItShouldInitializeProperties()
|
||||
{
|
||||
var response = new AnthropicFileDeleteResponse();
|
||||
|
||||
response.Id.Should().BeEmpty();
|
||||
response.Type.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithValues_ItShouldInitializePropertiesWithValues()
|
||||
{
|
||||
var id = "file-12345";
|
||||
var type = "file_deleted";
|
||||
|
||||
var response = new AnthropicFileDeleteResponse
|
||||
{
|
||||
Id = id,
|
||||
Type = type
|
||||
};
|
||||
|
||||
response.Id.Should().Be(id);
|
||||
response.Type.Should().Be(type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerialized_ItShouldMatchExpectedJson()
|
||||
{
|
||||
var response = new AnthropicFileDeleteResponse
|
||||
{
|
||||
Id = "file-12345",
|
||||
Type = "file_deleted"
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(response, JsonSerializationOptions.DefaultOptions);
|
||||
|
||||
JsonAssert.Equal(_testJson, json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonDeserialization_WhenDeserialized_ItShouldMatchExpectedObject()
|
||||
{
|
||||
var response = JsonSerializer.Deserialize<AnthropicFileDeleteResponse>(_testJson, JsonSerializationOptions.DefaultOptions);
|
||||
|
||||
response.Should().NotBeNull();
|
||||
response.Id.Should().Be("file-12345");
|
||||
response.Type.Should().Be("file_deleted");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
namespace AnthropicClient.Tests.Unit.Models;
|
||||
|
||||
public class AnthropicFileTests : SerializationTest
|
||||
{
|
||||
private readonly string _testJson = @"{
|
||||
""id"": ""file-123"",
|
||||
""type"": ""file"",
|
||||
""filename"": ""test.txt"",
|
||||
""created_at"": ""2023-10-01T00:00:00Z"",
|
||||
""size_bytes"": 1024,
|
||||
""mime_type"": ""text/plain"",
|
||||
""downloadable"": true
|
||||
}";
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalled_ItShouldInitializeProperties()
|
||||
{
|
||||
var result = new AnthropicFile();
|
||||
|
||||
result.Id.Should().BeEmpty();
|
||||
result.Type.Should().BeEmpty();
|
||||
result.Name.Should().BeEmpty();
|
||||
result.CreatedAt.Should().Be(DateTimeOffset.MinValue);
|
||||
result.Size.Should().Be(0);
|
||||
result.MimeType.Should().BeEmpty();
|
||||
result.Downloadable.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithValues_ItShouldInitializeProperties()
|
||||
{
|
||||
var file = new AnthropicFile
|
||||
{
|
||||
Id = "file-123",
|
||||
Type = "file",
|
||||
Name = "test.txt",
|
||||
CreatedAt = new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
Size = 1024,
|
||||
MimeType = "text/plain",
|
||||
Downloadable = true
|
||||
};
|
||||
|
||||
file.Id.Should().Be("file-123");
|
||||
file.Type.Should().Be("file");
|
||||
file.Name.Should().Be("test.txt");
|
||||
file.CreatedAt.Should().Be(new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
file.Size.Should().Be(1024);
|
||||
file.MimeType.Should().Be("text/plain");
|
||||
file.Downloadable.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
|
||||
{
|
||||
var file = new AnthropicFile
|
||||
{
|
||||
Id = "file-123",
|
||||
Type = "file",
|
||||
Name = "test.txt",
|
||||
CreatedAt = new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
Size = 1024,
|
||||
MimeType = "text/plain",
|
||||
Downloadable = true
|
||||
};
|
||||
|
||||
var json = Serialize(file);
|
||||
|
||||
var expectedJson = @"{
|
||||
""id"": ""file-123"",
|
||||
""type"": ""file"",
|
||||
""filename"": ""test.txt"",
|
||||
""created_at"": ""2023-10-01T00:00:00+00:00"",
|
||||
""size_bytes"": 1024,
|
||||
""mime_type"": ""text/plain"",
|
||||
""downloadable"": true
|
||||
}";
|
||||
|
||||
JsonAssert.Equal(expectedJson, json, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues()
|
||||
{
|
||||
var file = Deserialize<AnthropicFile>(_testJson);
|
||||
|
||||
file.Should().NotBeNull();
|
||||
file!.Id.Should().Be("file-123");
|
||||
file.Type.Should().Be("file");
|
||||
file.Name.Should().Be("test.txt");
|
||||
file.CreatedAt.Should().Be(new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
file.Size.Should().Be(1024);
|
||||
file.MimeType.Should().Be("text/plain");
|
||||
file.Downloadable.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
@@ -131,4 +131,64 @@ public class AnthropicModelsTests
|
||||
|
||||
actual.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Claude37Sonnet20250219_WhenCalled_ItShouldReturnExpectedValue()
|
||||
{
|
||||
var expected = "claude-3-7-sonnet-20250219";
|
||||
|
||||
var actual = AnthropicModels.Claude37Sonnet20250219;
|
||||
|
||||
actual.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Claude37SonnetLatest_WhenCalled_ItShouldReturnExpectedValue()
|
||||
{
|
||||
var expected = "claude-3-7-sonnet-latest";
|
||||
|
||||
var actual = AnthropicModels.Claude37SonnetLatest;
|
||||
|
||||
actual.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClaudeSonnet420250514_WhenCalled_ItShouldReturnExpectedValue()
|
||||
{
|
||||
var expected = "claude-sonnet-4-20250514";
|
||||
|
||||
var actual = AnthropicModels.ClaudeSonnet420250514;
|
||||
|
||||
actual.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClaudeSonnet40_WhenCalled_ItShouldReturnExpectedValue()
|
||||
{
|
||||
var expected = "claude-sonnet-4-0";
|
||||
|
||||
var actual = AnthropicModels.ClaudeSonnet40;
|
||||
|
||||
actual.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClaudeOpus420250514_WhenCalled_ItShouldReturnExpectedValue()
|
||||
{
|
||||
var expected = "claude-opus-4-20250514";
|
||||
|
||||
var actual = AnthropicModels.ClaudeOpus420250514;
|
||||
|
||||
actual.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClaudeOpus40_WhenCalled_ItShouldReturnExpectedValue()
|
||||
{
|
||||
var expected = "claude-opus-4-0";
|
||||
|
||||
var actual = AnthropicModels.ClaudeOpus40;
|
||||
|
||||
actual.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
namespace AnthropicClient.Tests.Unit.Models;
|
||||
|
||||
public class CreateFileRequestTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithBytes_ItShouldInitializeProperties()
|
||||
{
|
||||
var fileContent = new byte[] { 1, 2, 3 };
|
||||
var fileName = "test.txt";
|
||||
var fileType = "text/plain";
|
||||
|
||||
var request = new CreateFileRequest(fileContent, fileName, fileType);
|
||||
|
||||
request.File.Should().BeSameAs(fileContent);
|
||||
request.FileName.Should().Be(fileName);
|
||||
request.FileType.Should().Be(fileType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithStream_ItShouldInitializeProperties()
|
||||
{
|
||||
using var stream = new MemoryStream([1, 2, 3]);
|
||||
var fileName = "test.txt";
|
||||
var fileType = "text/plain";
|
||||
|
||||
var request = new CreateFileRequest(stream, fileName, fileType);
|
||||
|
||||
request.File.Should().BeEquivalentTo(new byte[] { 1, 2, 3 });
|
||||
request.FileName.Should().Be(fileName);
|
||||
request.FileType.Should().Be(fileType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithNullBytes_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
var act = () => new CreateFileRequest((byte[])null!, "test.txt", "text/plain");
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithBytesAndNullFileName_ItShouldThrowArgumentException()
|
||||
{
|
||||
var act = () => new CreateFileRequest([1, 2, 3], null!, "text/plain");
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithBytesAndFileNameIsEmpty_ItShouldThrowArgumentException()
|
||||
{
|
||||
var act = () => new CreateFileRequest([1, 2, 3], string.Empty, "text/plain");
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithBytesAndNullFileType_ItShouldThrowArgumentException()
|
||||
{
|
||||
var act = () => new CreateFileRequest([1, 2, 3], "test.txt", null!);
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithBytesAndFileTypeIsEmpty_ItShouldThrowArgumentException()
|
||||
{
|
||||
var act = () => new CreateFileRequest([1, 2, 3], "test.txt", string.Empty);
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithNullStream_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
var act = () => new CreateFileRequest((Stream)null!, "test.txt", "text/plain");
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithStreamAndNullFileName_ItShouldThrowArgumentException()
|
||||
{
|
||||
using var stream = new MemoryStream([1, 2, 3]);
|
||||
var act = () => new CreateFileRequest(stream, null!, "text/plain");
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithStreamAndFileNameIsEmpty_ItShouldThrowArgumentException()
|
||||
{
|
||||
using var stream = new MemoryStream([1, 2, 3]);
|
||||
var act = () => new CreateFileRequest(stream, string.Empty, "text/plain");
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithStreamAndNullFileType_ItShouldThrowArgumentException()
|
||||
{
|
||||
using var stream = new MemoryStream([1, 2, 3]);
|
||||
var act = () => new CreateFileRequest(stream, "test.txt", null!);
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithStreamAndFileTypeIsEmpty_ItShouldThrowArgumentException()
|
||||
{
|
||||
using var stream = new MemoryStream([1, 2, 3]);
|
||||
var act = () => new CreateFileRequest(stream, "test.txt", string.Empty);
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
namespace AnthropicClient.Tests.Unit.Models;
|
||||
|
||||
public class FileSourceTests : SerializationTest
|
||||
{
|
||||
private readonly string _testJson = @"{
|
||||
""file_id"": ""id"",
|
||||
""type"": ""file""
|
||||
}";
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalled_ItShouldInitializeProperties()
|
||||
{
|
||||
var result = new FileSource();
|
||||
|
||||
result.Type.Should().Be("file");
|
||||
result.Id.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithValues_ItShouldInitializeProperties()
|
||||
{
|
||||
var id = "id";
|
||||
var type = "type";
|
||||
|
||||
var result = new FileSource()
|
||||
{
|
||||
Id = id,
|
||||
Type = type,
|
||||
};
|
||||
|
||||
result.Id.Should().Be(id);
|
||||
result.Type.Should().Be(type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithId_ItShouldInitializeProperties()
|
||||
{
|
||||
var id = "id";
|
||||
|
||||
var result = new FileSource(id);
|
||||
|
||||
result.Id.Should().Be(id);
|
||||
result.Type.Should().Be("file");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
|
||||
{
|
||||
var source = new FileSource() { Id = "id" };
|
||||
|
||||
var result = Serialize<Source>(source);
|
||||
|
||||
JsonAssert.Equal(_testJson, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues()
|
||||
{
|
||||
var result = Deserialize<Source>(_testJson);
|
||||
|
||||
result.Should().BeEquivalentTo(new FileSource() { Id = "id" });
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ public class ImageContentTests : SerializationTest
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithCacheControlAndMediatTypeIsNull_ItShouldThrowArgumentNullException()
|
||||
public void Constructor_WhenCalledWithCacheControlAndMediaTypeIsNull_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
var expectedData = "data";
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
@@ -98,6 +98,49 @@ public class ImageContentTests : SerializationTest
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithSource_ItShouldInitializeSource()
|
||||
{
|
||||
var source = new ImageSource("image/png", "data");
|
||||
|
||||
var result = new ImageContent(source);
|
||||
|
||||
result.Source.Should().BeSameAs(source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithSourceAndCacheControl_ItShouldInitializeSourceAndCacheControl()
|
||||
{
|
||||
var source = new ImageSource("image/png", "data");
|
||||
var cacheControl = new EphemeralCacheControl();
|
||||
|
||||
var result = new ImageContent(source, cacheControl);
|
||||
|
||||
result.Source.Should().BeSameAs(source);
|
||||
result.CacheControl.Should().BeSameAs(cacheControl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenSourceIsNull_ItShouldThrowArgumentNullException()
|
||||
{
|
||||
Source? source = null;
|
||||
|
||||
var action = () => new ImageContent(source!);
|
||||
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCacheControlIsNull_ItShouldThrowNullException()
|
||||
{
|
||||
var source = new ImageSource("image/png", "data");
|
||||
CacheControl? cacheControl = null;
|
||||
|
||||
var action = () => new ImageContent(source, cacheControl!);
|
||||
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
|
||||
{
|
||||
|
||||
@@ -19,4 +19,10 @@ public class SourceTypeTests
|
||||
{
|
||||
SourceType.Text.Should().Be("text");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_WhenCalled_ItShouldReturnCorrectValue()
|
||||
{
|
||||
SourceType.File.Should().Be("file");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace AnthropicClient.Tests.Unit.Models;
|
||||
|
||||
public class UrlSourceTests : SerializationTest
|
||||
{
|
||||
private readonly string _testJson = @"{
|
||||
""type"": ""url"",
|
||||
""url"": ""https://example.com/document.pdf""
|
||||
}";
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalled_ItShouldInitializeProperties()
|
||||
{
|
||||
var result = new UrlSource();
|
||||
|
||||
result.Url.Should().BeEmpty();
|
||||
result.Type.Should().Be("url");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithValues_ItShouldInitializeProperties()
|
||||
{
|
||||
var url = "https://example.com/document.pdf";
|
||||
var type = "type";
|
||||
|
||||
var result = new UrlSource()
|
||||
{
|
||||
Url = url,
|
||||
Type = type,
|
||||
};
|
||||
|
||||
result.Url.Should().Be(url);
|
||||
result.Type.Should().Be(type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhenCalledWithUrl_ItShouldInitializeProperties()
|
||||
{
|
||||
var url = "https://example.com/document.pdf";
|
||||
|
||||
var result = new UrlSource(url);
|
||||
|
||||
result.Url.Should().Be(url);
|
||||
result.Type.Should().Be("url");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
|
||||
{
|
||||
var url = "https://example.com/document.pdf";
|
||||
var source = new UrlSource(url);
|
||||
|
||||
var result = Serialize<Source>(source);
|
||||
|
||||
JsonAssert.Equal(_testJson, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonDeserialization_WhenDeserialized_ItShouldMatchExpectedObject()
|
||||
{
|
||||
var result = Deserialize<Source>(_testJson);
|
||||
|
||||
result.Should().BeEquivalentTo(new UrlSource("https://example.com/document.pdf"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user