2024-06-28 16:30:09 -05:00
|
|
|
using System.Net.Http.Headers;
|
2025-05-19 19:52:16 +00:00
|
|
|
using System.Runtime.CompilerServices;
|
2024-06-28 16:30:09 -05:00
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
|
|
|
|
|
using AnthropicClient.Json;
|
|
|
|
|
using AnthropicClient.Models;
|
2024-07-01 22:46:02 -05:00
|
|
|
using AnthropicClient.Utils;
|
2024-06-28 16:30:09 -05:00
|
|
|
|
|
|
|
|
namespace AnthropicClient;
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc cref="IAnthropicApiClient"/>
|
|
|
|
|
public class AnthropicApiClient : IAnthropicApiClient
|
|
|
|
|
{
|
|
|
|
|
private const string BaseUrl = "https://api.anthropic.com/v1/";
|
|
|
|
|
private const string ApiKeyHeader = "x-api-key";
|
|
|
|
|
private const string MessagesEndpoint = "messages";
|
2025-01-08 20:39:16 -06:00
|
|
|
private string CountTokensEndpoint => $"{MessagesEndpoint}/count_tokens";
|
2025-01-08 20:37:58 -06:00
|
|
|
private string MessageBatchesEndpoint => $"{MessagesEndpoint}/batches";
|
2025-01-05 21:20:27 -06:00
|
|
|
private const string ModelsEndpoint = "models";
|
2025-07-11 23:14:41 -05:00
|
|
|
private const string FilesEndpoint = "files";
|
2024-06-28 16:30:09 -05:00
|
|
|
private const string JsonContentType = "application/json";
|
|
|
|
|
private const string EventPrefix = "event:";
|
|
|
|
|
private const string DataPrefix = "data:";
|
|
|
|
|
private readonly Dictionary<string, string> _defaultHeaders = new()
|
|
|
|
|
{
|
|
|
|
|
{ "anthropic-version", "2023-06-01" },
|
|
|
|
|
};
|
|
|
|
|
private readonly HttpClient _httpClient;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Initializes a new instance of the <see cref="AnthropicApiClient"/> class.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="apiKey">The API key to use for the client.</param>
|
|
|
|
|
/// <param name="httpClient">The HTTP client to use for the client.</param>
|
|
|
|
|
/// <exception cref="ArgumentNullException">Thrown when the API key or HTTP client is null.</exception>
|
|
|
|
|
/// <returns>A new instance of the <see cref="AnthropicApiClient"/> class.</returns>
|
|
|
|
|
public AnthropicApiClient(string apiKey, HttpClient httpClient)
|
|
|
|
|
{
|
|
|
|
|
ArgumentValidator.ThrowIfNull(apiKey, nameof(apiKey));
|
|
|
|
|
ArgumentValidator.ThrowIfNull(httpClient, nameof(httpClient));
|
|
|
|
|
|
|
|
|
|
_httpClient = httpClient;
|
|
|
|
|
_httpClient.BaseAddress = new Uri(BaseUrl);
|
|
|
|
|
_httpClient.DefaultRequestHeaders.Add(ApiKeyHeader, apiKey);
|
|
|
|
|
_httpClient.DefaultRequestHeaders
|
|
|
|
|
.Accept
|
|
|
|
|
.Add(new MediaTypeWithQualityHeaderValue(JsonContentType));
|
|
|
|
|
|
|
|
|
|
foreach (var pair in _defaultHeaders)
|
|
|
|
|
{
|
|
|
|
|
_httpClient.DefaultRequestHeaders.Add(pair.Key, pair.Value);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-05 16:57:30 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async Task<AnthropicResult<MessageResponse>> CreateMessageAsync(MessageRequest request, CancellationToken cancellationToken = default)
|
2024-06-28 16:30:09 -05:00
|
|
|
{
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync(MessagesEndpoint, request, cancellationToken);
|
2024-06-28 16:30:09 -05:00
|
|
|
var anthropicHeaders = new AnthropicHeaders(response.Headers);
|
2025-05-19 15:14:11 -05:00
|
|
|
var responseContent = await response.Content.ReadAsStringAsync();
|
2024-06-28 16:30:09 -05:00
|
|
|
|
|
|
|
|
if (response.IsSuccessStatusCode is false)
|
|
|
|
|
{
|
|
|
|
|
var error = Deserialize<AnthropicError>(responseContent) ?? new AnthropicError();
|
2024-07-03 16:57:27 -05:00
|
|
|
return AnthropicResult<MessageResponse>.Failure(error, anthropicHeaders);
|
2024-06-28 16:30:09 -05:00
|
|
|
}
|
|
|
|
|
|
2024-07-03 16:57:27 -05:00
|
|
|
var msgResponse = Deserialize<MessageResponse>(responseContent) ?? new MessageResponse();
|
2024-07-01 22:46:02 -05:00
|
|
|
|
2024-07-01 21:00:27 -05:00
|
|
|
if (request.Tools is not null && request.Tools.Count > 0)
|
|
|
|
|
{
|
2024-07-03 16:57:27 -05:00
|
|
|
msgResponse.ToolCall = GetToolCall(msgResponse, request.Tools);
|
2024-07-01 21:00:27 -05:00
|
|
|
}
|
2024-07-01 22:46:02 -05:00
|
|
|
|
2024-07-03 16:57:27 -05:00
|
|
|
return AnthropicResult<MessageResponse>.Success(msgResponse, anthropicHeaders);
|
2024-06-28 16:30:09 -05:00
|
|
|
}
|
|
|
|
|
|
2025-01-05 16:57:30 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRequest request, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
2024-06-28 16:30:09 -05:00
|
|
|
{
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync(MessagesEndpoint, request, cancellationToken);
|
2024-07-19 21:15:59 -05:00
|
|
|
|
|
|
|
|
if (response.IsSuccessStatusCode is false)
|
|
|
|
|
{
|
2025-05-19 15:14:11 -05:00
|
|
|
var errorContent = await response.Content.ReadAsStringAsync();
|
|
|
|
|
var error = Deserialize<AnthropicError>(errorContent) ?? new AnthropicError();
|
2024-07-19 21:15:59 -05:00
|
|
|
yield return new AnthropicEvent(EventType.Error, new ErrorEventData(error.Error));
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-28 16:30:09 -05:00
|
|
|
var anthropicHeaders = new AnthropicHeaders(response.Headers);
|
|
|
|
|
|
|
|
|
|
using var responseContent = await response.Content.ReadAsStreamAsync();
|
|
|
|
|
using var streamReader = new StreamReader(responseContent);
|
|
|
|
|
|
2024-07-03 16:57:27 -05:00
|
|
|
MessageResponse? msgResponse = null;
|
2024-06-28 16:30:09 -05:00
|
|
|
Content? content = null;
|
|
|
|
|
var toolInputJsonStringBuilder = new StringBuilder();
|
|
|
|
|
var currentEvent = new AnthropicEvent();
|
|
|
|
|
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
var line = await streamReader.ReadLineAsync();
|
2024-07-01 22:46:02 -05:00
|
|
|
|
2024-06-28 16:30:09 -05:00
|
|
|
// I know...this is not pretty, but here is why...
|
|
|
|
|
// as events are being yielded I want to also
|
2024-07-03 16:57:27 -05:00
|
|
|
// build up the complete response
|
2024-06-28 16:30:09 -05:00
|
|
|
// so I can yield it as a special event to make tool
|
|
|
|
|
// calling easier to handle
|
2024-07-01 22:46:02 -05:00
|
|
|
|
2024-07-03 16:57:27 -05:00
|
|
|
// initialize response on message start
|
2024-06-28 16:30:09 -05:00
|
|
|
if (currentEvent.Type is EventType.MessageStart && currentEvent.Data is MessageStartEventData msgStartData)
|
|
|
|
|
{
|
2024-07-03 16:57:27 -05:00
|
|
|
msgResponse = msgStartData.Message;
|
2024-06-28 16:30:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// initialize content block on content block start
|
|
|
|
|
if (currentEvent.Type is EventType.ContentBlockStart && currentEvent.Data is ContentStartEventData contentStartData)
|
|
|
|
|
{
|
|
|
|
|
content = contentStartData.ContentBlock;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// update content block with deltas based on
|
|
|
|
|
// current content type and delta type
|
|
|
|
|
if (currentEvent.Type is EventType.ContentBlockDelta && currentEvent.Data is ContentDeltaEventData contentDeltaData)
|
|
|
|
|
{
|
2025-06-14 22:24:44 -05:00
|
|
|
if (content is TextContent textContent)
|
2024-06-28 16:30:09 -05:00
|
|
|
{
|
2025-06-14 22:24:44 -05:00
|
|
|
if (contentDeltaData.Delta is TextDelta textDelta)
|
|
|
|
|
{
|
|
|
|
|
var newText = textContent.Text + textDelta.Text;
|
|
|
|
|
|
|
|
|
|
content = new TextContent(newText)
|
|
|
|
|
{
|
|
|
|
|
Citations = textContent.Citations,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (contentDeltaData.Delta is CitationDelta citationDelta)
|
|
|
|
|
{
|
|
|
|
|
var citations = new List<Citation>()
|
|
|
|
|
{
|
|
|
|
|
citationDelta.Citation,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (textContent.Citations is not null)
|
|
|
|
|
{
|
|
|
|
|
citations.AddRange(textContent.Citations);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var newContent = new TextContent(textContent.Text)
|
|
|
|
|
{
|
|
|
|
|
Citations = [.. citations],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
content = newContent;
|
|
|
|
|
}
|
2024-06-28 16:30:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (content is ToolUseContent toolUseContent && contentDeltaData.Delta is JsonDelta jsonDelta)
|
|
|
|
|
{
|
|
|
|
|
toolInputJsonStringBuilder.Append(jsonDelta.PartialJson);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// finalize content block on content block stop
|
2024-07-03 16:57:27 -05:00
|
|
|
// and add it to the response
|
2024-06-28 16:30:09 -05:00
|
|
|
if (currentEvent.Type is EventType.ContentBlockStop)
|
|
|
|
|
{
|
2024-07-03 16:57:27 -05:00
|
|
|
if (content is not null && msgResponse is not null)
|
2024-06-28 16:30:09 -05:00
|
|
|
{
|
|
|
|
|
if (content is TextContent textContent)
|
|
|
|
|
{
|
2024-07-03 16:57:27 -05:00
|
|
|
msgResponse.Content.Add(textContent);
|
2024-06-28 16:30:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (content is ToolUseContent toolUseContent)
|
|
|
|
|
{
|
|
|
|
|
var input = Deserialize<Dictionary<string, object?>>(toolInputJsonStringBuilder.ToString());
|
|
|
|
|
var newToolUseContent = new ToolUseContent()
|
|
|
|
|
{
|
|
|
|
|
Id = toolUseContent.Id,
|
|
|
|
|
Name = toolUseContent.Name,
|
|
|
|
|
Input = input!,
|
|
|
|
|
};
|
|
|
|
|
|
2024-07-03 16:57:27 -05:00
|
|
|
msgResponse.Content.Add(newToolUseContent);
|
2024-06-28 16:30:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
content = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-03 16:57:27 -05:00
|
|
|
// update response with message delta data
|
2024-06-28 16:30:09 -05:00
|
|
|
if (
|
2024-07-01 22:46:02 -05:00
|
|
|
currentEvent.Type is EventType.MessageDelta &&
|
2024-06-28 16:30:09 -05:00
|
|
|
currentEvent.Data is MessageDeltaEventData msgDeltaData &&
|
2024-07-03 16:57:27 -05:00
|
|
|
msgResponse is not null
|
2024-06-28 16:30:09 -05:00
|
|
|
)
|
|
|
|
|
{
|
2024-07-03 16:57:27 -05:00
|
|
|
var existingUsage = msgResponse.Usage;
|
|
|
|
|
var newUsage = new Usage()
|
2024-06-29 22:07:49 -05:00
|
|
|
{
|
|
|
|
|
InputTokens = existingUsage.InputTokens + msgDeltaData.Usage.InputTokens,
|
|
|
|
|
OutputTokens = existingUsage.OutputTokens + msgDeltaData.Usage.OutputTokens,
|
2024-08-16 09:21:43 -05:00
|
|
|
CacheCreationInputTokens = existingUsage.CacheCreationInputTokens + msgDeltaData.Usage.CacheCreationInputTokens,
|
|
|
|
|
CacheReadInputTokens = existingUsage.CacheReadInputTokens + msgDeltaData.Usage.CacheReadInputTokens,
|
2024-06-29 22:07:49 -05:00
|
|
|
};
|
|
|
|
|
|
2024-07-03 16:57:27 -05:00
|
|
|
msgResponse = new MessageResponse()
|
2024-06-28 16:30:09 -05:00
|
|
|
{
|
2024-07-03 16:57:27 -05:00
|
|
|
Id = msgResponse.Id,
|
|
|
|
|
Model = msgResponse.Model,
|
|
|
|
|
Role = msgResponse.Role,
|
2024-06-28 16:30:09 -05:00
|
|
|
StopReason = msgDeltaData.Delta.StopReason,
|
|
|
|
|
StopSequence = msgDeltaData.Delta.StopSequence,
|
2024-07-03 16:57:27 -05:00
|
|
|
Type = msgResponse.Type,
|
2024-06-29 22:07:49 -05:00
|
|
|
Usage = newUsage,
|
2024-07-03 16:57:27 -05:00
|
|
|
Content = msgResponse.Content,
|
2024-06-28 16:30:09 -05:00
|
|
|
};
|
2024-07-01 21:00:27 -05:00
|
|
|
|
|
|
|
|
if (request.Tools is not null && request.Tools.Count > 0)
|
|
|
|
|
{
|
2024-07-03 16:57:27 -05:00
|
|
|
msgResponse.ToolCall = GetToolCall(msgResponse, request.Tools);
|
2024-07-01 21:00:27 -05:00
|
|
|
}
|
2024-06-28 16:30:09 -05:00
|
|
|
}
|
|
|
|
|
|
2024-07-03 16:57:27 -05:00
|
|
|
// yield response on message stop
|
|
|
|
|
if (currentEvent.Type is EventType.MessageStop && msgResponse is not null)
|
2024-06-28 16:30:09 -05:00
|
|
|
{
|
2024-07-03 16:57:27 -05:00
|
|
|
var eventData = new MessageCompleteEventData(msgResponse, anthropicHeaders);
|
2024-06-28 16:30:09 -05:00
|
|
|
yield return new AnthropicEvent(EventType.MessageComplete, eventData);
|
2024-07-03 16:57:27 -05:00
|
|
|
msgResponse = null;
|
2024-06-28 16:30:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (line is null)
|
|
|
|
|
{
|
2024-07-03 00:11:48 -05:00
|
|
|
if (string.IsNullOrWhiteSpace(currentEvent.Type) is false)
|
|
|
|
|
{
|
|
|
|
|
yield return currentEvent;
|
|
|
|
|
currentEvent = new AnthropicEvent();
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-28 16:30:09 -05:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (line == string.Empty)
|
|
|
|
|
{
|
|
|
|
|
yield return currentEvent;
|
|
|
|
|
currentEvent = new AnthropicEvent();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (line.StartsWith(EventPrefix))
|
|
|
|
|
{
|
|
|
|
|
var eventType = line.Substring(EventPrefix.Length).Trim();
|
|
|
|
|
currentEvent = new AnthropicEvent(eventType, currentEvent.Data);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (line.StartsWith(DataPrefix))
|
|
|
|
|
{
|
|
|
|
|
var eventData = line.Substring(DataPrefix.Length).Trim();
|
|
|
|
|
var eventDataJson = Deserialize<EventData>(eventData);
|
|
|
|
|
currentEvent = new AnthropicEvent(currentEvent.Type, eventDataJson!);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
} while (true);
|
|
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-08 20:37:58 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async Task<AnthropicResult<MessageBatchResponse>> CreateMessageBatchAsync(MessageBatchRequest request, CancellationToken cancellationToken = default)
|
2025-01-08 20:37:58 -06:00
|
|
|
{
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync(MessageBatchesEndpoint, request, cancellationToken);
|
2025-05-19 15:14:11 -05:00
|
|
|
return await CreateResultAsync<MessageBatchResponse>(response);
|
2025-01-08 20:37:58 -06:00
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-08 23:15:24 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async Task<AnthropicResult<MessageBatchResponse>> GetMessageBatchAsync(string batchId, CancellationToken cancellationToken = default)
|
2025-01-08 23:15:24 -06:00
|
|
|
{
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}", cancellationToken: cancellationToken);
|
2025-05-19 15:14:11 -05:00
|
|
|
return await CreateResultAsync<MessageBatchResponse>(response);
|
2025-01-08 23:15:24 -06:00
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-12 13:46:05 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async Task<AnthropicResult<Page<MessageBatchResponse>>> ListMessageBatchesAsync(PagingRequest? request = null, CancellationToken cancellationToken = default)
|
2025-01-12 13:46:05 -06:00
|
|
|
{
|
|
|
|
|
var pagingRequest = request ?? new PagingRequest();
|
|
|
|
|
var endpoint = $"{MessageBatchesEndpoint}?{pagingRequest.ToQueryParameters()}";
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken);
|
2025-05-19 15:14:11 -05:00
|
|
|
return await CreateResultAsync<Page<MessageBatchResponse>>(response);
|
2025-01-12 13:46:05 -06:00
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-13 12:42:23 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async IAsyncEnumerable<AnthropicResult<Page<MessageBatchResponse>>> ListAllMessageBatchesAsync(int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
2025-01-13 12:42:23 -06:00
|
|
|
{
|
2025-05-19 19:52:16 +00:00
|
|
|
await foreach (var result in GetAllPagesAsync<MessageBatchResponse>(MessageBatchesEndpoint, limit, cancellationToken))
|
2025-01-13 12:42:23 -06:00
|
|
|
{
|
|
|
|
|
yield return result;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-13 13:11:47 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async Task<AnthropicResult<MessageBatchResponse>> CancelMessageBatchAsync(string batchId, CancellationToken cancellationToken = default)
|
2025-01-13 13:11:47 -06:00
|
|
|
{
|
|
|
|
|
var endpoint = $"{MessageBatchesEndpoint}/{batchId}/cancel";
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync(endpoint, HttpMethod.Post, cancellationToken);
|
2025-05-19 15:14:11 -05:00
|
|
|
return await CreateResultAsync<MessageBatchResponse>(response);
|
2025-01-13 13:11:47 -06:00
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-14 13:41:32 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async Task<AnthropicResult<MessageBatchDeleteResponse>> DeleteMessageBatchAsync(string batchId, CancellationToken cancellationToken = default)
|
2025-01-14 13:41:32 -06:00
|
|
|
{
|
|
|
|
|
var endpoint = $"{MessageBatchesEndpoint}/{batchId}";
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync(endpoint, HttpMethod.Delete, cancellationToken);
|
2025-05-19 15:14:11 -05:00
|
|
|
return await CreateResultAsync<MessageBatchDeleteResponse>(response);
|
2025-01-14 13:41:32 -06:00
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-09 23:17:41 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async Task<AnthropicResult<IAsyncEnumerable<MessageBatchResultItem>>> GetMessageBatchResultsAsync(string batchId, CancellationToken cancellationToken = default)
|
2025-01-09 23:17:41 -06:00
|
|
|
{
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}/results", cancellationToken: cancellationToken);
|
2025-01-09 23:17:41 -06:00
|
|
|
var anthropicHeaders = new AnthropicHeaders(response.Headers);
|
|
|
|
|
|
|
|
|
|
if (response.IsSuccessStatusCode is false)
|
|
|
|
|
{
|
2025-05-19 15:14:11 -05:00
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
2025-01-09 23:17:41 -06:00
|
|
|
var error = Deserialize<AnthropicError>(content) ?? new AnthropicError();
|
|
|
|
|
return AnthropicResult<IAsyncEnumerable<MessageBatchResultItem>>.Failure(error, anthropicHeaders);
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-09 23:40:55 -06:00
|
|
|
return AnthropicResult<IAsyncEnumerable<MessageBatchResultItem>>.Success(ReadResultsAsync(), anthropicHeaders);
|
|
|
|
|
|
2025-05-19 15:28:08 -05:00
|
|
|
async IAsyncEnumerable<MessageBatchResultItem> ReadResultsAsync()
|
2025-01-09 23:17:41 -06:00
|
|
|
{
|
2025-05-19 15:14:11 -05:00
|
|
|
using var responseContent = await response.Content.ReadAsStreamAsync();
|
2025-01-09 23:17:41 -06:00
|
|
|
using var streamReader = new StreamReader(responseContent);
|
|
|
|
|
|
|
|
|
|
var line = await streamReader.ReadLineAsync();
|
|
|
|
|
|
|
|
|
|
while (line is not null)
|
|
|
|
|
{
|
|
|
|
|
var resultItem = Deserialize<MessageBatchResultItem>(line) ?? new MessageBatchResultItem();
|
|
|
|
|
yield return resultItem;
|
|
|
|
|
|
|
|
|
|
line = await streamReader.ReadLineAsync();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-05 16:57:30 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async Task<AnthropicResult<TokenCountResponse>> CountMessageTokensAsync(CountMessageTokensRequest request, CancellationToken cancellationToken = default)
|
2025-01-02 20:40:38 -06:00
|
|
|
{
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync(CountTokensEndpoint, request, cancellationToken);
|
2025-05-19 15:14:11 -05:00
|
|
|
return await CreateResultAsync<TokenCountResponse>(response);
|
2025-01-02 20:40:38 -06:00
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-05 21:20:27 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async Task<AnthropicResult<Page<AnthropicModel>>> ListModelsAsync(PagingRequest? request = null, CancellationToken cancellationToken = default)
|
2025-01-05 21:20:27 -06:00
|
|
|
{
|
|
|
|
|
var pagingRequest = request ?? new PagingRequest();
|
|
|
|
|
var endpoint = $"{ModelsEndpoint}?{pagingRequest.ToQueryParameters()}";
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken);
|
2025-05-19 15:14:11 -05:00
|
|
|
return await CreateResultAsync<Page<AnthropicModel>>(response);
|
2025-01-05 21:20:27 -06:00
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-05 21:20:27 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async IAsyncEnumerable<AnthropicResult<Page<AnthropicModel>>> ListAllModelsAsync(int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
2025-01-05 21:20:27 -06:00
|
|
|
{
|
2025-05-19 19:52:16 +00:00
|
|
|
await foreach (var result in GetAllPagesAsync<AnthropicModel>(ModelsEndpoint, limit, cancellationToken))
|
2025-01-05 21:20:27 -06:00
|
|
|
{
|
2025-01-13 12:42:23 -06:00
|
|
|
yield return result;
|
|
|
|
|
}
|
2025-01-05 21:20:27 -06:00
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-01-05 21:36:34 -06:00
|
|
|
/// <inheritdoc/>
|
2025-05-19 19:52:16 +00:00
|
|
|
public async Task<AnthropicResult<AnthropicModel>> GetModelAsync(string modelId, CancellationToken cancellationToken = default)
|
2025-01-05 21:36:34 -06:00
|
|
|
{
|
|
|
|
|
var endpoint = $"{ModelsEndpoint}/{modelId}";
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken);
|
2025-05-19 15:14:11 -05:00
|
|
|
return await CreateResultAsync<AnthropicModel>(response);
|
2025-01-05 21:36:34 -06:00
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-07-11 23:14:41 -05:00
|
|
|
/// <inheritdoc/>
|
|
|
|
|
public async Task<AnthropicResult<AnthropicFile>> CreateFileAsync(CreateFileRequest request, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var response = await SendFileRequestAsync(FilesEndpoint, request, cancellationToken);
|
|
|
|
|
return await CreateResultAsync<AnthropicFile>(response);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-13 21:12:37 -05:00
|
|
|
/// <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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-13 22:33:11 -05:00
|
|
|
/// <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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-19 19:52:16 +00:00
|
|
|
private async IAsyncEnumerable<AnthropicResult<Page<T>>> GetAllPagesAsync<T>(string endpoint, int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
2025-01-13 12:42:23 -06:00
|
|
|
{
|
|
|
|
|
var pagingRequest = new PagingRequest(limit: limit);
|
|
|
|
|
string Endpoint() => $"{endpoint}?{pagingRequest.ToQueryParameters()}";
|
|
|
|
|
bool hasMore;
|
|
|
|
|
|
|
|
|
|
do
|
|
|
|
|
{
|
2025-05-19 19:52:16 +00:00
|
|
|
var response = await SendRequestAsync(Endpoint(), cancellationToken: cancellationToken);
|
2025-01-13 12:42:23 -06:00
|
|
|
var anthropicHeaders = new AnthropicHeaders(response.Headers);
|
2025-05-19 15:14:11 -05:00
|
|
|
var responseContent = await response.Content.ReadAsStringAsync();
|
2025-01-13 12:42:23 -06:00
|
|
|
|
|
|
|
|
if (response.IsSuccessStatusCode is false)
|
|
|
|
|
{
|
|
|
|
|
var error = Deserialize<AnthropicError>(responseContent) ?? new AnthropicError();
|
|
|
|
|
yield return AnthropicResult<Page<T>>.Failure(error, anthropicHeaders);
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var page = Deserialize<Page<T>>(responseContent) ?? new Page<T>();
|
|
|
|
|
|
|
|
|
|
if (page.HasMore && page.LastId is not null)
|
|
|
|
|
{
|
|
|
|
|
hasMore = true;
|
|
|
|
|
pagingRequest = new PagingRequest(limit: limit, afterId: page.LastId);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
hasMore = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
yield return AnthropicResult<Page<T>>.Success(page, anthropicHeaders);
|
|
|
|
|
} while (hasMore);
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-03 16:57:27 -05:00
|
|
|
private ToolCall? GetToolCall(MessageResponse response, List<Tool> tools)
|
2024-07-01 21:00:27 -05:00
|
|
|
{
|
|
|
|
|
var toolUse = response.Content.OfType<ToolUseContent>().FirstOrDefault();
|
|
|
|
|
|
|
|
|
|
if (toolUse is null)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var tool = tools.FirstOrDefault(t => t.Name == toolUse.Name);
|
|
|
|
|
|
|
|
|
|
if (tool is null)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new ToolCall(tool, toolUse);
|
|
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
|
|
|
|
private async Task<AnthropicResult<T>> CreateResultAsync<T>(HttpResponseMessage response) where T : new()
|
2025-01-14 13:14:14 -06:00
|
|
|
{
|
|
|
|
|
var anthropicHeaders = new AnthropicHeaders(response.Headers);
|
2025-05-19 15:14:11 -05:00
|
|
|
var responseContent = await response.Content.ReadAsStringAsync();
|
2025-01-14 13:14:14 -06:00
|
|
|
|
|
|
|
|
if (response.IsSuccessStatusCode is false)
|
|
|
|
|
{
|
|
|
|
|
var error = Deserialize<AnthropicError>(responseContent) ?? new AnthropicError();
|
|
|
|
|
return AnthropicResult<T>.Failure(error, anthropicHeaders);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var model = Deserialize<T>(responseContent) ?? new T();
|
|
|
|
|
return AnthropicResult<T>.Success(model, anthropicHeaders);
|
|
|
|
|
}
|
2025-05-19 15:14:11 -05:00
|
|
|
|
2025-05-19 19:52:16 +00:00
|
|
|
private async Task<HttpResponseMessage> SendRequestAsync(string endpoint, HttpMethod? method = null, CancellationToken cancellationToken = default)
|
2025-01-05 21:20:27 -06:00
|
|
|
{
|
2025-01-13 13:11:47 -06:00
|
|
|
var request = new HttpRequestMessage(method ?? HttpMethod.Get, endpoint);
|
2025-05-19 19:52:16 +00:00
|
|
|
return await _httpClient.SendAsync(request, cancellationToken);
|
2025-01-05 21:20:27 -06:00
|
|
|
}
|
|
|
|
|
|
2025-05-19 19:52:16 +00:00
|
|
|
private async Task<HttpResponseMessage> SendRequestAsync<T>(string endpoint, T request, CancellationToken cancellationToken = default)
|
2024-08-18 14:59:47 -05:00
|
|
|
{
|
2024-08-18 12:53:08 -05:00
|
|
|
var requestJson = Serialize(request);
|
|
|
|
|
var requestContent = new StringContent(requestJson, Encoding.UTF8, JsonContentType);
|
2025-05-19 19:52:16 +00:00
|
|
|
return await _httpClient.PostAsync(endpoint, requestContent, cancellationToken);
|
2024-06-28 16:30:09 -05:00
|
|
|
}
|
|
|
|
|
|
2025-07-11 23:14:41 -05:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-28 16:30:09 -05:00
|
|
|
private string Serialize<T>(T obj) => JsonSerializer.Serialize(obj, JsonSerializationOptions.DefaultOptions);
|
|
|
|
|
private T? Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json, JsonSerializationOptions.DefaultOptions);
|
|
|
|
|
}
|