diff --git a/Anthropic.SDK.Tests/SkillsTests.cs b/Anthropic.SDK.Tests/SkillsTests.cs index 05b2cff..95d4f16 100644 --- a/Anthropic.SDK.Tests/SkillsTests.cs +++ b/Anthropic.SDK.Tests/SkillsTests.cs @@ -335,5 +335,13 @@ c is BashCodeExecutionToolResultContent bashCodeExecutionToolResultContent && bashCodeExecutionToolResultContent.Content is BashCodeExecutionOutputContent bashCodeExecutionOutputContent && !string.IsNullOrEmpty(bashCodeExecutionOutputContent.FileId)) != null); } + + [TestMethod] + public async Task GetSkills() + { + var client = new AnthropicClient(); + var skills = await client.Skills.ListSkillsAsync(); + Assert.IsNotNull(skills); + } } } diff --git a/Anthropic.SDK/AnthropicClient.cs b/Anthropic.SDK/AnthropicClient.cs index a6e54c3..67889d7 100644 --- a/Anthropic.SDK/AnthropicClient.cs +++ b/Anthropic.SDK/AnthropicClient.cs @@ -35,7 +35,7 @@ public class AnthropicClient : IDisposable /// /// Version of the Anthropic Beta API /// - public string AnthropicBetaVersion { get; set; } = "prompt-caching-2024-07-31,message-batches-2024-09-24,computer-use-2024-10-22,pdfs-2024-09-25,output-128k-2025-02-19,mcp-client-2025-04-04,code-execution-2025-08-25,skills-2025-10-02,files-api-2025-04-14"; + public string AnthropicBetaVersion { get; set; } = "prompt-caching-2024-07-31,message-batches-2024-09-24,computer-use-2024-10-22,pdfs-2024-09-25,output-128k-2025-02-19,mcp-client-2025-04-04,code-execution-2025-08-25,files-api-2025-04-14"; /// /// The API authentication information to use for API calls diff --git a/Anthropic.SDK/BaseEndpoint.cs b/Anthropic.SDK/BaseEndpoint.cs index e3cd285..21d438a 100644 --- a/Anthropic.SDK/BaseEndpoint.cs +++ b/Anthropic.SDK/BaseEndpoint.cs @@ -112,9 +112,9 @@ private static void TryParseHeaderValue(HttpResponseMessage message, string h /// Makes an HTTP request and deserializes the response to the specified type without custom converters. /// protected async Task HttpRequestSimple(string url = null, HttpMethod verb = null, - object postData = null, CancellationToken ctx = default) + object postData = null, Dictionary additionalHeaders = null, CancellationToken ctx = default) { - var response = await HttpRequestRaw(url, verb, postData, false, ctx).ConfigureAwait(false); + var response = await HttpRequestRaw(url, verb, postData, false, additionalHeaders, ctx).ConfigureAwait(false); // Optimization: Deserialize directly from HTTP response stream // Avoids intermediate string allocation and UTF8 encoding conversion @@ -128,14 +128,14 @@ protected async Task HttpRequestSimple(string url = null, HttpMethod verb return res; } - /// - /// Makes a raw HTTP request and returns the response. - /// - protected async Task HttpRequestRaw(string url = null, HttpMethod verb = null, - object postData = null, bool streaming = false, CancellationToken ctx = default) - { - return await HttpRequestRaw(url, verb, postData, streaming, null, ctx).ConfigureAwait(false); - } + ///// + ///// Makes a raw HTTP request and returns the response. + ///// + //protected async Task HttpRequestRaw(string url = null, HttpMethod verb = null, + // object postData = null, bool streaming = false, CancellationToken ctx = default) + //{ + // return await HttpRequestRaw(url, verb, postData, streaming, null, ctx).ConfigureAwait(false); + //} /// /// Makes a raw HTTP request and returns the response with additional headers. diff --git a/Anthropic.SDK/Batches/BatchesEndpoint.cs b/Anthropic.SDK/Batches/BatchesEndpoint.cs index 6da5a8a..db547da 100644 --- a/Anthropic.SDK/Batches/BatchesEndpoint.cs +++ b/Anthropic.SDK/Batches/BatchesEndpoint.cs @@ -24,7 +24,7 @@ internal BatchesEndpoint(AnthropicClient client) : base(client) { } /// public async Task CreateBatchAsync(List batches, CancellationToken ctx = default) { - var response = await HttpRequestSimple(Url, HttpMethod.Post, new { requests = batches }, ctx).ConfigureAwait(false); + var response = await HttpRequestSimple(Url, HttpMethod.Post, new { requests = batches }, null, ctx).ConfigureAwait(false); return response; } @@ -36,7 +36,7 @@ public async Task CreateBatchAsync(List batches, Ca /// public async Task CancelBatchAsync(string batchId, CancellationToken ctx = default) { - var response = await HttpRequestSimple(Url + $"/{batchId}/cancel", HttpMethod.Post, null, ctx).ConfigureAwait(false); + var response = await HttpRequestSimple(Url + $"/{batchId}/cancel", HttpMethod.Post, null, null, ctx).ConfigureAwait(false); return response; } @@ -48,7 +48,7 @@ public async Task CancelBatchAsync(string batchId, CancellationTo /// public async Task RetrieveBatchStatusAsync(string batchId, CancellationToken ctx = default) { - var response = await HttpRequestSimple(Url + $"/{batchId}", HttpMethod.Get, null, ctx).ConfigureAwait(false); + var response = await HttpRequestSimple(Url + $"/{batchId}", HttpMethod.Get, null, null, ctx).ConfigureAwait(false); return response; } @@ -104,7 +104,7 @@ public async Task ListBatchesAsync(string beforeId = null, string aft url += $"&after_id={afterId}"; } - var response = await HttpRequestSimple(url, HttpMethod.Get, null, ctx).ConfigureAwait(false); + var response = await HttpRequestSimple(url, HttpMethod.Get, null, null, ctx).ConfigureAwait(false); return response; } diff --git a/Anthropic.SDK/EndpointBase.cs b/Anthropic.SDK/EndpointBase.cs index 6f584f8..3437d1a 100644 --- a/Anthropic.SDK/EndpointBase.cs +++ b/Anthropic.SDK/EndpointBase.cs @@ -109,7 +109,7 @@ protected async IAsyncEnumerable HttpStreamingRequestBatches(string u HttpMethod verb = null, object postData = null, [EnumeratorCancellation] CancellationToken ctx = default) { - var response = await HttpRequestRaw(url, verb, postData, streaming: true, ctx).ConfigureAwait(false); + var response = await HttpRequestRaw(url, verb, postData, streaming: true, null, ctx).ConfigureAwait(false); #if NET6_0_OR_GREATER await using var stream = await response.Content.ReadAsStreamAsync(ctx).ConfigureAwait(false); #else @@ -139,7 +139,7 @@ protected async IAsyncEnumerable HttpStreamingRequestBatchesJsonl(string HttpMethod verb = null, object postData = null, [EnumeratorCancellation] CancellationToken ctx = default) { - var response = await HttpRequestRaw(url, verb, postData, streaming: true, ctx).ConfigureAwait(false); + var response = await HttpRequestRaw(url, verb, postData, streaming: true, null, ctx).ConfigureAwait(false); #if NET6_0_OR_GREATER await using var stream = await response.Content.ReadAsStreamAsync(ctx).ConfigureAwait(false); #else diff --git a/Anthropic.SDK/Files/FilesEndpoint.cs b/Anthropic.SDK/Files/FilesEndpoint.cs index 5a0917d..0ea1f12 100644 --- a/Anthropic.SDK/Files/FilesEndpoint.cs +++ b/Anthropic.SDK/Files/FilesEndpoint.cs @@ -48,7 +48,7 @@ public async Task ListFilesAsync( queryParams.Add($"after_id={afterId}"); var queryString = "?" + string.Join("&", queryParams); - return await HttpRequestSimple($"{Endpoint}{queryString}", HttpMethod.Get, null, cancellationToken); + return await HttpRequestSimple($"{Url}{queryString}", HttpMethod.Get, null, null, cancellationToken); } /// @@ -67,7 +67,7 @@ public async Task GetFileMetadataAsync( throw new ArgumentNullException(nameof(fileId), "File ID cannot be null or empty."); } - return await HttpRequestSimple($"{Endpoint}/{fileId}", HttpMethod.Get, null, cancellationToken); + return await HttpRequestSimple($"{Url}/{fileId}", HttpMethod.Get, null, null, cancellationToken); } /// @@ -86,7 +86,7 @@ public async Task DeleteFileAsync( throw new ArgumentNullException(nameof(fileId), "File ID cannot be null or empty."); } - return await HttpRequestSimple($"{Endpoint}/{fileId}", HttpMethod.Delete, null, cancellationToken); + return await HttpRequestSimple($"{Url}/{fileId}", HttpMethod.Delete, null, null, cancellationToken); } /// @@ -157,7 +157,7 @@ public async Task UploadFileBytesAsync(byte[] fileBytes, string fi fileContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); content.Add(fileContent, "file", fileName); - var result = await HttpRequestSimple(Url, HttpMethod.Post, content, ctx).ConfigureAwait(false); + var result = await HttpRequestSimple(Url, HttpMethod.Post, content, null, ctx).ConfigureAwait(false); return result; } @@ -194,7 +194,7 @@ public async Task UploadFileStreamAsync(Stream fileStream, string streamContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); content.Add(streamContent, "file", fileName); - var result = await HttpRequestSimple(Url, HttpMethod.Post, content, ctx).ConfigureAwait(false); + var result = await HttpRequestSimple(Url, HttpMethod.Post, content, null, ctx).ConfigureAwait(false); return result; } @@ -215,7 +215,7 @@ public async Task DownloadFileAsync(string fileId, string outputPath = n } var url = $"{Url}/{fileId}/content"; - var response = await HttpRequestRaw(url, HttpMethod.Get, null, streaming: false, ctx).ConfigureAwait(false); + var response = await HttpRequestRaw(url, HttpMethod.Get, null, streaming: false, null, ctx).ConfigureAwait(false); #if NET6_0_OR_GREATER var content = await response.Content.ReadAsByteArrayAsync(ctx).ConfigureAwait(false); @@ -263,7 +263,7 @@ public async Task DownloadFileToStreamAsync(string fileId, Stream outputStream, } var url = $"{Url}/{fileId}/content"; - var response = await HttpRequestRaw(url, HttpMethod.Get, null, streaming: true, ctx).ConfigureAwait(false); + var response = await HttpRequestRaw(url, HttpMethod.Get, null, streaming: true, null, ctx).ConfigureAwait(false); #if NET6_0_OR_GREATER await using var stream = await response.Content.ReadAsStreamAsync(ctx).ConfigureAwait(false); diff --git a/Anthropic.SDK/Messaging/MessagesEndpoint.cs b/Anthropic.SDK/Messaging/MessagesEndpoint.cs index 2ec5ef3..84e963f 100644 --- a/Anthropic.SDK/Messaging/MessagesEndpoint.cs +++ b/Anthropic.SDK/Messaging/MessagesEndpoint.cs @@ -31,26 +31,7 @@ public async Task GetClaudeMessageAsync(MessageParameters param parameters.Stream = false; // Check if interleaved thinking is needed and add the header - Dictionary additionalHeaders = null; - if (parameters.Thinking?.UseInterleavedThinking == true) - { - // Add the interleaved thinking beta header to the existing beta features - var existingBeta = Client.AnthropicBetaVersion; - var interleavedBeta = "interleaved-thinking-2025-05-14"; - - // Combine with existing beta features if they don't already include interleaved thinking - if (!existingBeta.Contains(interleavedBeta)) - { - var combinedBeta = string.IsNullOrWhiteSpace(existingBeta) - ? interleavedBeta - : $"{existingBeta},{interleavedBeta}"; - - additionalHeaders = new Dictionary - { - ["anthropic-beta"] = combinedBeta - }; - } - } + var additionalHeaders = SetAdditionalHeaders(parameters); var response = await HttpRequestMessages(Url, HttpMethod.Post, parameters, additionalHeaders, ctx).ConfigureAwait(false); @@ -118,27 +99,7 @@ public async IAsyncEnumerable StreamClaudeMessageAsync(MessageP parameters.Stream = true; - // Check if interleaved thinking is needed and add the header - Dictionary additionalHeaders = null; - if (parameters.Thinking?.UseInterleavedThinking == true) - { - // Add the interleaved thinking beta header to the existing beta features - var existingBeta = Client.AnthropicBetaVersion; - var interleavedBeta = "interleaved-thinking-2025-05-14"; - - // Combine with existing beta features if they don't already include interleaved thinking - if (!existingBeta.Contains(interleavedBeta)) - { - var combinedBeta = string.IsNullOrWhiteSpace(existingBeta) - ? interleavedBeta - : $"{existingBeta},{interleavedBeta}"; - - additionalHeaders = new Dictionary - { - ["anthropic-beta"] = combinedBeta - }; - } - } + var additionalHeaders = SetAdditionalHeaders(parameters); var toolCalls = new List(); var arguments = string.Empty; @@ -178,6 +139,51 @@ public async IAsyncEnumerable StreamClaudeMessageAsync(MessageP yield return result; } } + + private Dictionary SetAdditionalHeaders(MessageParameters parameters) + { + // Check if interleaved thinking is needed and add the header + Dictionary additionalHeaders = null; + if (parameters.Thinking?.UseInterleavedThinking == true) + { + // Add the interleaved thinking beta header to the existing beta features + var existingBeta = Client.AnthropicBetaVersion; + var interleavedBeta = "interleaved-thinking-2025-05-14"; + // Combine with existing beta features if they don't already include interleaved thinking + if (!existingBeta.Contains(interleavedBeta)) + { + var combinedBeta = string.IsNullOrWhiteSpace(existingBeta) + ? interleavedBeta + : $"{existingBeta},{interleavedBeta}"; + + additionalHeaders = new Dictionary + { + ["anthropic-beta"] = combinedBeta + }; + } + } + if (parameters.Container != null) + { + if (additionalHeaders == null) + { + additionalHeaders = new Dictionary(); + } + var existingBeta = Client.AnthropicBetaVersion; + var skillsBeta = "skills-2025-10-02"; + // Combine with existing beta features if they don't already include skills + if (!existingBeta.Contains(skillsBeta)) + { + var combinedBeta = string.IsNullOrWhiteSpace(existingBeta) + ? skillsBeta + : $"{existingBeta},{skillsBeta}"; + additionalHeaders["anthropic-beta"] = combinedBeta; + } + } + + return additionalHeaders; + } + + /// /// Makes a call to count the number of tokens in a request. /// diff --git a/Anthropic.SDK/Models/ModelsEndpoint.cs b/Anthropic.SDK/Models/ModelsEndpoint.cs index e17a8a5..f6ec8a1 100644 --- a/Anthropic.SDK/Models/ModelsEndpoint.cs +++ b/Anthropic.SDK/Models/ModelsEndpoint.cs @@ -40,7 +40,7 @@ public async Task ListModelsAsync(string beforeId = null, string afte url += $"&after_id={afterId}"; } - var response = await HttpRequestSimple(url, HttpMethod.Get, null, ctx).ConfigureAwait(false); + var response = await HttpRequestSimple(url, HttpMethod.Get, null, null, ctx).ConfigureAwait(false); return response; } @@ -52,7 +52,7 @@ public async Task ListModelsAsync(string beforeId = null, string afte /// public async Task GetModelAsync(string modelId, CancellationToken ctx = default) { - var response = await HttpRequestSimple(Url + $"/{modelId}", HttpMethod.Get, null, ctx).ConfigureAwait(false); + var response = await HttpRequestSimple(Url + $"/{modelId}", HttpMethod.Get, null, null, ctx).ConfigureAwait(false); return response; } diff --git a/Anthropic.SDK/Skills/SkillsEndpoint.cs b/Anthropic.SDK/Skills/SkillsEndpoint.cs index 9ba738f..f7db2a1 100644 --- a/Anthropic.SDK/Skills/SkillsEndpoint.cs +++ b/Anthropic.SDK/Skills/SkillsEndpoint.cs @@ -87,7 +87,8 @@ public async Task CreateSkillAsync( content.Add(fileContent, "files[]", relativePath); } - return await HttpRequestSimple(Url, HttpMethod.Post, content, cancellationToken).ConfigureAwait(false); + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple(Url, HttpMethod.Post, content, additionalHeaders, cancellationToken).ConfigureAwait(false); } /// @@ -132,8 +133,8 @@ public async Task CreateSkillFromZipAsync( var fileContent = new ByteArrayContent(fileBytes); fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); content.Add(fileContent, "files[]", Path.GetFileName(zipFilePath)); - - return await HttpRequestSimple(Url, HttpMethod.Post, content, cancellationToken).ConfigureAwait(false); + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple(Url, HttpMethod.Post, content, additionalHeaders, cancellationToken).ConfigureAwait(false); } /// @@ -177,8 +178,8 @@ public async Task CreateSkillFromStreamsAsync( streamContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); content.Add(streamContent, "files[]", filename); } - - return await HttpRequestSimple(Url, HttpMethod.Post, content, cancellationToken).ConfigureAwait(false); + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple(Url, HttpMethod.Post, content, additionalHeaders, cancellationToken).ConfigureAwait(false); } /// @@ -242,8 +243,8 @@ public async Task CreateSkillVersionAsync( // Use the relative path as the filename in the multipart form content.Add(fileContent, "files[]", relativePath); } - - return await HttpRequestSimple($"{Endpoint}/{skillId}/versions", HttpMethod.Post, content, cancellationToken).ConfigureAwait(false); + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple($"{Url}/{skillId}/versions", HttpMethod.Post, content, additionalHeaders, cancellationToken).ConfigureAwait(false); } /// @@ -288,7 +289,8 @@ public async Task CreateSkillVersionFromZipAsync( fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); content.Add(fileContent, "files[]", Path.GetFileName(zipFilePath)); - return await HttpRequestSimple($"{Endpoint}/{skillId}/versions", HttpMethod.Post, content, cancellationToken).ConfigureAwait(false); + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple($"{Url}/{skillId}/versions", HttpMethod.Post, content, additionalHeaders, cancellationToken).ConfigureAwait(false); } /// @@ -332,7 +334,8 @@ public async Task CreateSkillVersionFromStreamsAsync( content.Add(streamContent, "files[]", filename); } - return await HttpRequestSimple($"{Endpoint}/{skillId}/versions", HttpMethod.Post, content, cancellationToken).ConfigureAwait(false); + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple($"{Url}/{skillId}/versions", HttpMethod.Post, content, additionalHeaders, cancellationToken).ConfigureAwait(false); } /// @@ -366,7 +369,9 @@ public async Task ListSkillVersionsAsync( queryParams.Add($"page={page}"); var queryString = "?" + string.Join("&", queryParams); - return await HttpRequestSimple($"{Endpoint}/{skillId}/versions{queryString}", HttpMethod.Get, null, cancellationToken); + + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple($"{Url}/{skillId}/versions{queryString}", HttpMethod.Get, null, additionalHeaders, cancellationToken); } /// @@ -392,7 +397,8 @@ public async Task GetSkillVersionAsync( throw new ArgumentNullException(nameof(version), "Version cannot be null or empty."); } - return await HttpRequestSimple($"{Endpoint}/{skillId}/versions/{version}", HttpMethod.Get, null, cancellationToken); + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple($"{Url}/{skillId}/versions/{version}", HttpMethod.Get, null, additionalHeaders, cancellationToken); } /// @@ -418,7 +424,8 @@ public async Task DeleteSkillVersionAsync( throw new ArgumentNullException(nameof(version), "Version cannot be null or empty."); } - return await HttpRequestSimple($"{Endpoint}/{skillId}/versions/{version}", HttpMethod.Delete, null, cancellationToken); + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple($"{Url}/{skillId}/versions/{version}", HttpMethod.Delete, null, additionalHeaders, cancellationToken); } /// @@ -447,7 +454,9 @@ public async Task ListSkillsAsync( queryParams.Add($"source={source}"); var queryString = "?" + string.Join("&", queryParams); - return await HttpRequestSimple($"{Endpoint}{queryString}", HttpMethod.Get, null, cancellationToken); + + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple($"{Url}{queryString}", HttpMethod.Get, null, additionalHeaders, cancellationToken); } /// @@ -466,7 +475,8 @@ public async Task GetSkillAsync( throw new ArgumentNullException(nameof(skillId), "Skill ID cannot be null or empty."); } - return await HttpRequestSimple($"{Endpoint}/{skillId}", HttpMethod.Get, null, cancellationToken); + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple($"{Url}/{skillId}", HttpMethod.Get, null, additionalHeaders, cancellationToken); } /// @@ -485,7 +495,8 @@ public async Task DeleteSkillAsync( throw new ArgumentNullException(nameof(skillId), "Skill ID cannot be null or empty."); } - return await HttpRequestSimple($"{Endpoint}/{skillId}", HttpMethod.Delete, null, cancellationToken); + var additionalHeaders = SetSkillsHeaders(); + return await HttpRequestSimple($"{Url}/{skillId}", HttpMethod.Delete, null, additionalHeaders, cancellationToken); } /// @@ -551,5 +562,23 @@ private static string GetRelativePath(string basePath, string targetPath) // Convert forward slashes to platform-specific separators return relativePath.Replace('/', Path.DirectorySeparatorChar); } + + private Dictionary SetSkillsHeaders() + { + // Check if interleaved thinking is needed and add the header + Dictionary additionalHeaders = new Dictionary(); + var existingBeta = Client.AnthropicBetaVersion; + var skillsBeta = "skills-2025-10-02"; + // Combine with existing beta features if they don't already include skills + if (!existingBeta.Contains(skillsBeta)) + { + var combinedBeta = string.IsNullOrWhiteSpace(existingBeta) + ? skillsBeta + : $"{existingBeta},{skillsBeta}"; + additionalHeaders["anthropic-beta"] = combinedBeta; + } + + return additionalHeaders; + } } } diff --git a/Anthropic.SDK/VertexAIEndpointBase.cs b/Anthropic.SDK/VertexAIEndpointBase.cs index 1d29725..2fa7064 100644 --- a/Anthropic.SDK/VertexAIEndpointBase.cs +++ b/Anthropic.SDK/VertexAIEndpointBase.cs @@ -186,7 +186,7 @@ protected override async IAsyncEnumerable HttpStreamingRequestM HttpMethod verb = null, object postData = null, [EnumeratorCancellation] CancellationToken ctx = default) { - var response = await HttpRequestRaw(url, verb, postData, streaming: true, ctx).ConfigureAwait(false); + var response = await HttpRequestRaw(url, verb, postData, streaming: true, null, ctx).ConfigureAwait(false); #if NET6_0_OR_GREATER await using var stream = await response.Content.ReadAsStreamAsync(ctx).ConfigureAwait(false); #else