Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
018264d
feat: initial implementation of creating a file via the Files API
StevanFreeborn Jul 12, 2025
ab6d455
fix: remove unnecessary usings
StevanFreeborn Jul 13, 2025
31539a8
tests: add create file request tests
StevanFreeborn Jul 13, 2025
94c3028
docs: fix xml comments
StevanFreeborn Jul 13, 2025
a5746a0
tests: add integration test for creating file
StevanFreeborn Jul 13, 2025
98236bb
tests: add end to end test
StevanFreeborn Jul 13, 2025
a5eaa4b
docs: add note about files beta status
StevanFreeborn Jul 13, 2025
09f56c4
tests: add tests for anthropic file model
StevanFreeborn Jul 13, 2025
26908d3
feat: implement listing a page of files
StevanFreeborn Jul 14, 2025
9cf50e1
feat: implement list all files method
StevanFreeborn Jul 14, 2025
63278bb
feat: implement `GetFileInfoAsync`, `GetFileAsync`, and `DeleteFileAs…
StevanFreeborn Jul 14, 2025
3a50c06
tests: add tests for sad path in new files methods
StevanFreeborn Jul 15, 2025
0a2514f
refactor: remove unnecessary if checks and add test to handle getting…
StevanFreeborn Jul 15, 2025
e821b35
chore: run dotnet format
StevanFreeborn Jul 15, 2025
1f304b8
fix: use sync copy to method
StevanFreeborn Jul 15, 2025
348976d
docs: update files api section with examples for each method
StevanFreeborn Jul 15, 2025
e0c6e59
tests: remove unnecessary reason string
StevanFreeborn Jul 15, 2025
45df2db
feat: add support for file source and url source
StevanFreeborn Jul 15, 2025
3c03e70
tests: add test for file source constructor with id
StevanFreeborn Jul 15, 2025
3dd6d6c
chore: run dotnet format
StevanFreeborn Jul 15, 2025
8d2b023
feat: add missing model constants
StevanFreeborn Jul 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
70 changes: 70 additions & 0 deletions src/AnthropicClient/AnthropicApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:";
Expand Down Expand Up @@ -380,6 +381,64 @@ public async Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId,
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);
Expand Down Expand Up @@ -462,6 +521,17 @@ private async Task<HttpResponseMessage> SendRequestAsync<T>(string endpoint, T r
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);
}
49 changes: 49 additions & 0 deletions src/AnthropicClient/IAnthropicApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
14 changes: 14 additions & 0 deletions src/AnthropicClient/Json/SourceConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public override Source Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS
{
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}")
};
Expand Down Expand Up @@ -54,6 +56,18 @@ public override void Write(Utf8JsonWriter writer, Source value, JsonSerializerOp
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);
}
}
51 changes: 51 additions & 0 deletions src/AnthropicClient/Models/AnthropicFile.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
17 changes: 17 additions & 0 deletions src/AnthropicClient/Models/AnthropicFileDeleteResponse.cs
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading