diff --git a/Anthropic.SDK.Tests/ChatOptionsExtensionsTests.cs b/Anthropic.SDK.Tests/ChatOptionsExtensionsTests.cs new file mode 100644 index 0000000..1e1c1e3 --- /dev/null +++ b/Anthropic.SDK.Tests/ChatOptionsExtensionsTests.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using Anthropic.SDK; +using Anthropic.SDK.Constants; +using Anthropic.SDK.Extensions; +using Anthropic.SDK.Messaging; +using Microsoft.Extensions.AI; + +namespace Anthropic.SDK.Tests +{ + [TestClass] + public class ChatOptionsExtensionsTests + { + [TestMethod] + public void WithThinking_SetsThinkingParameters() + { + // Arrange + var options = new ChatOptions(); + var budgetTokens = 4000; + + // Act + var result = options.WithThinking(budgetTokens); + + // Assert + Assert.AreSame(options, result); // Should return same instance for fluent chaining + var thinkingParams = options.GetThinkingParameters(); + Assert.IsNotNull(thinkingParams); + Assert.AreEqual(budgetTokens, thinkingParams.BudgetTokens); + Assert.AreEqual("enabled", thinkingParams.Type); + Assert.IsFalse(thinkingParams.UseInterleavedThinking); + } + + [TestMethod] + public void WithThinking_WithThinkingParametersObject_SetsThinkingParameters() + { + // Arrange + var options = new ChatOptions(); + var thinkingParams = new ThinkingParameters { BudgetTokens = 3000 }; + + // Act + var result = options.WithThinking(thinkingParams); + + // Assert + Assert.AreSame(options, result); + var retrievedParams = options.GetThinkingParameters(); + Assert.AreSame(thinkingParams, retrievedParams); + Assert.AreEqual(3000, retrievedParams.BudgetTokens); + Assert.IsFalse(retrievedParams.UseInterleavedThinking); + } + + [TestMethod] + public void WithInterleavedThinking_SetsInterleavedThinkingParameters() + { + // Arrange + var options = new ChatOptions(); + var budgetTokens = 8000; // Can exceed max_tokens with interleaved thinking + + // Act + var result = options.WithInterleavedThinking(budgetTokens); + + // Assert + Assert.AreSame(options, result); // Should return same instance for fluent chaining + var thinkingParams = options.GetThinkingParameters(); + Assert.IsNotNull(thinkingParams); + Assert.AreEqual(budgetTokens, thinkingParams.BudgetTokens); + Assert.AreEqual("enabled", thinkingParams.Type); + Assert.IsTrue(thinkingParams.UseInterleavedThinking); + } + + [TestMethod] + public void WithInterleavedThinking_WithThinkingParametersObject_SetsInterleavedThinkingParameters() + { + // Arrange + var options = new ChatOptions(); + var thinkingParams = new ThinkingParameters { BudgetTokens = 10000 }; + + // Act + var result = options.WithInterleavedThinking(thinkingParams); + + // Assert + Assert.AreSame(options, result); + var retrievedParams = options.GetThinkingParameters(); + Assert.AreSame(thinkingParams, retrievedParams); + Assert.AreEqual(10000, retrievedParams.BudgetTokens); + Assert.IsTrue(retrievedParams.UseInterleavedThinking); + } + + [TestMethod] + public void WithThinking_NullOptions_ThrowsArgumentNullException() + { + // Arrange + ChatOptions options = null; + + // Act & Assert + Assert.ThrowsException(() => options.WithThinking(4000)); + } + + [TestMethod] + public void WithInterleavedThinking_NullOptions_ThrowsArgumentNullException() + { + // Arrange + ChatOptions options = null; + + // Act & Assert + Assert.ThrowsException(() => options.WithInterleavedThinking(8000)); + } + + [TestMethod] + public void WithThinking_NullThinkingParameters_ThrowsArgumentNullException() + { + // Arrange + var options = new ChatOptions(); + + // Act & Assert + Assert.ThrowsException(() => options.WithThinking(null)); + } + + [TestMethod] + public void WithInterleavedThinking_NullThinkingParameters_ThrowsArgumentNullException() + { + // Arrange + var options = new ChatOptions(); + + // Act & Assert + Assert.ThrowsException(() => options.WithInterleavedThinking(null)); + } + + [TestMethod] + public void WithThinking_ZeroBudgetTokens_ThrowsArgumentOutOfRangeException() + { + // Arrange + var options = new ChatOptions(); + + // Act & Assert + Assert.ThrowsException(() => options.WithThinking(0)); + } + + [TestMethod] + public void WithInterleavedThinking_ZeroBudgetTokens_ThrowsArgumentOutOfRangeException() + { + // Arrange + var options = new ChatOptions(); + + // Act & Assert + Assert.ThrowsException(() => options.WithInterleavedThinking(0)); + } + + [TestMethod] + public void WithThinking_NegativeBudgetTokens_ThrowsArgumentOutOfRangeException() + { + // Arrange + var options = new ChatOptions(); + + // Act & Assert + Assert.ThrowsException(() => options.WithThinking(-1000)); + } + + [TestMethod] + public void WithInterleavedThinking_NegativeBudgetTokens_ThrowsArgumentOutOfRangeException() + { + // Arrange + var options = new ChatOptions(); + + // Act & Assert + Assert.ThrowsException(() => options.WithInterleavedThinking(-1000)); + } + + [TestMethod] + public void GetThinkingParameters_NoThinkingSet_ReturnsNull() + { + // Arrange + var options = new ChatOptions(); + + // Act + var result = options.GetThinkingParameters(); + + // Assert + Assert.IsNull(result); + } + + [TestMethod] + public void GetThinkingParameters_NullOptions_ReturnsNull() + { + // Arrange + ChatOptions options = null; + + // Act + var result = options.GetThinkingParameters(); + + // Assert + Assert.IsNull(result); + } + + [TestMethod] + public void WithThinking_OverwritesPreviousThinkingParameters() + { + // Arrange + var options = new ChatOptions(); + options.WithThinking(3000); + + // Act + options.WithThinking(4000); + + // Assert + var thinkingParams = options.GetThinkingParameters(); + Assert.IsNotNull(thinkingParams); + Assert.AreEqual(4000, thinkingParams.BudgetTokens); + Assert.IsFalse(thinkingParams.UseInterleavedThinking); + } + + [TestMethod] + public void WithInterleavedThinking_OverwritesPreviousThinkingParameters() + { + // Arrange + var options = new ChatOptions(); + options.WithThinking(3000); + + // Act + options.WithInterleavedThinking(8000); + + // Assert + var thinkingParams = options.GetThinkingParameters(); + Assert.IsNotNull(thinkingParams); + Assert.AreEqual(8000, thinkingParams.BudgetTokens); + Assert.IsTrue(thinkingParams.UseInterleavedThinking); + } + + [TestMethod] + public void WithThinking_FluentChaining_Works() + { + // Arrange & Act + var options = new ChatOptions + { + ModelId = AnthropicModels.Claude37Sonnet, + MaxOutputTokens = 4096, + Temperature = 1.0f + }.WithThinking(4000); + + // Assert + Assert.AreEqual(AnthropicModels.Claude37Sonnet, options.ModelId); + Assert.AreEqual(4096, options.MaxOutputTokens); + Assert.AreEqual(1.0f, options.Temperature); + + var thinkingParams = options.GetThinkingParameters(); + Assert.IsNotNull(thinkingParams); + Assert.AreEqual(4000, thinkingParams.BudgetTokens); + Assert.IsFalse(thinkingParams.UseInterleavedThinking); + } + + [TestMethod] + public void WithInterleavedThinking_FluentChaining_Works() + { + // Arrange & Act + var options = new ChatOptions + { + ModelId = AnthropicModels.Claude37Sonnet, + MaxOutputTokens = 4096, + Temperature = 1.0f + }.WithInterleavedThinking(8000); + + // Assert + Assert.AreEqual(AnthropicModels.Claude37Sonnet, options.ModelId); + Assert.AreEqual(4096, options.MaxOutputTokens); + Assert.AreEqual(1.0f, options.Temperature); + + var thinkingParams = options.GetThinkingParameters(); + Assert.IsNotNull(thinkingParams); + Assert.AreEqual(8000, thinkingParams.BudgetTokens); + Assert.IsTrue(thinkingParams.UseInterleavedThinking); + } + + [TestMethod] + public void ChatClientHelper_MapsThinkingParametersCorrectly() + { + // Arrange + var client = new AnthropicClient().Messages; + var messages = new List + { + new ChatMessage(ChatRole.User, "Test message") + }; + var options = new ChatOptions + { + ModelId = AnthropicModels.Claude37Sonnet, + MaxOutputTokens = 4096, + Temperature = 1.0f + }.WithThinking(3000); + + // Act + var messageParams = ChatClientHelper.CreateMessageParameters(client, messages, options); + + // Assert + Assert.IsNotNull(messageParams.Thinking); + Assert.AreEqual(3000, messageParams.Thinking.BudgetTokens); + Assert.AreEqual("enabled", messageParams.Thinking.Type); + Assert.IsFalse(messageParams.Thinking.UseInterleavedThinking); + Assert.AreEqual(AnthropicModels.Claude37Sonnet, messageParams.Model); + Assert.AreEqual(4096, messageParams.MaxTokens); + } + + [TestMethod] + public void ChatClientHelper_MapsInterleavedThinkingParametersCorrectly() + { + // Arrange + var client = new AnthropicClient().Messages; + var messages = new List + { + new ChatMessage(ChatRole.User, "Test message") + }; + var options = new ChatOptions + { + ModelId = AnthropicModels.Claude37Sonnet, + MaxOutputTokens = 4096, + Temperature = 1.0f + }.WithInterleavedThinking(8000); + + // Act + var messageParams = ChatClientHelper.CreateMessageParameters(client, messages, options); + + // Assert + Assert.IsNotNull(messageParams.Thinking); + Assert.AreEqual(8000, messageParams.Thinking.BudgetTokens); + Assert.AreEqual("enabled", messageParams.Thinking.Type); + Assert.IsTrue(messageParams.Thinking.UseInterleavedThinking); + Assert.AreEqual(AnthropicModels.Claude37Sonnet, messageParams.Model); + Assert.AreEqual(4096, messageParams.MaxTokens); + } + } +} \ No newline at end of file diff --git a/Anthropic.SDK.Tests/Messages.ChatClient.cs b/Anthropic.SDK.Tests/Messages.ChatClient.cs index 0e396b7..ea74afa 100644 --- a/Anthropic.SDK.Tests/Messages.ChatClient.cs +++ b/Anthropic.SDK.Tests/Messages.ChatClient.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Text; using Anthropic.SDK.Constants; +using Anthropic.SDK.Extensions; using Anthropic.SDK.Messaging; using Microsoft.Extensions.AI; using TextContent = Microsoft.Extensions.AI.TextContent; @@ -48,6 +49,75 @@ public async Task TestNonStreamingMessageWithInstructions() Assert.IsTrue(res.Text.Contains("pirate") is true, res.Text); } + [TestMethod] + public async Task TestNonStreamingThinkingWithExtensionMethods() + { + IChatClient client = new AnthropicClient().Messages; + + List messages = new() + { + new ChatMessage(ChatRole.User, "How many r's are in the word strawberry?") + }; + + ChatOptions options = new ChatOptions() + { + ModelId = AnthropicModels.Claude37Sonnet, + MaxOutputTokens = 20000, + Temperature = 1.0f, + }.WithThinking(16000); + + var res = await client.GetResponseAsync(messages, options); + Assert.IsTrue(res.Text.Contains("3") is true, res.Text); + messages.AddMessages(res); + messages.Add(new ChatMessage(ChatRole.User, "and how many letters total?")); + res = await client.GetResponseAsync(messages, options); + Assert.IsTrue(res.Text?.Contains("10") is true, res.Text); + } + + [TestMethod] + public async Task TestThinkingStreamingWithExtensionMethods() + { + IChatClient client = new AnthropicClient().Messages; + + List messages = new() + { + new ChatMessage(ChatRole.User, "How many r's are in the word strawberry?") + }; + + ChatOptions options = new ChatOptions() + { + ModelId = AnthropicModels.Claude37Sonnet, + MaxOutputTokens = 20000, + Temperature = 1.0f, + }.WithThinking(16000); + + List updates = new(); + StringBuilder sb = new(); + await foreach (var res in client.GetStreamingResponseAsync(messages, options)) + { + updates.Add(res); + sb.Append(res); + } + + Assert.IsTrue(sb.ToString().Contains("3") is true, sb.ToString()); + + messages.AddMessages(updates); + + Assert.IsTrue(messages.Last().Contents.OfType().Any()); + + messages.Add(new ChatMessage(ChatRole.User, "and how many letters total?")); + + updates.Clear(); + await foreach (var res in client.GetStreamingResponseAsync(messages, options)) + { + updates.Add(res); + } + var text = string.Join("", + updates.SelectMany(p => p.Contents.OfType()).Select(p => p.Text)); + + Assert.IsTrue(text.Contains("10") is true, text); + } + [TestMethod] public async Task TestNonStreamingThinkingConversation() { diff --git a/Anthropic.SDK/BaseEndpoint.cs b/Anthropic.SDK/BaseEndpoint.cs index 90f5877..901692e 100644 --- a/Anthropic.SDK/BaseEndpoint.cs +++ b/Anthropic.SDK/BaseEndpoint.cs @@ -48,7 +48,16 @@ protected async Task ReadResponseContentAsync(HttpResponseMessage respon protected async Task HttpRequestMessages(string url = null, HttpMethod verb = null, object postData = null, CancellationToken ctx = default) { - var response = await HttpRequestRaw(url, verb, postData, false, ctx).ConfigureAwait(false); + return await HttpRequestMessages(url, verb, postData, null, ctx).ConfigureAwait(false); + } + + /// + /// Makes an HTTP request and deserializes the response to the specified type with additional headers. + /// + protected async Task HttpRequestMessages(string url = null, HttpMethod verb = null, + object postData = null, Dictionary additionalHeaders = null, CancellationToken ctx = default) + { + var response = await HttpRequestRaw(url, verb, postData, false, additionalHeaders, ctx).ConfigureAwait(false); string resultAsString = await ReadResponseContentAsync(response, ctx).ConfigureAwait(false); var options = new JsonSerializerOptions @@ -108,6 +117,15 @@ protected async Task HttpRequestSimple(string url = null, HttpMethod verb /// 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. + /// + protected async Task HttpRequestRaw(string url = null, HttpMethod verb = null, + object postData = null, bool streaming = false, Dictionary additionalHeaders = null, CancellationToken ctx = default) { if (string.IsNullOrEmpty(url)) url = this.Url; @@ -116,6 +134,15 @@ protected async Task HttpRequestRaw(string url = null, Http string resultAsString = null; var req = new HttpRequestMessage(verb, url); + // Add additional headers if provided + if (additionalHeaders != null) + { + foreach (var header in additionalHeaders) + { + req.Headers.Add(header.Key, header.Value); + } + } + if (postData != null) { if (postData is HttpContent content) diff --git a/Anthropic.SDK/EndpointBase.cs b/Anthropic.SDK/EndpointBase.cs index d2339d8..e8e3281 100644 --- a/Anthropic.SDK/EndpointBase.cs +++ b/Anthropic.SDK/EndpointBase.cs @@ -204,7 +204,20 @@ 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); + await foreach (var item in HttpStreamingRequestMessages(url, verb, postData, null, ctx)) + { + yield return item; + } + } + + /// + /// Makes a streaming HTTP request and returns the response as an async enumerable of MessageResponse with additional headers. + /// + protected async IAsyncEnumerable HttpStreamingRequestMessages(string url = null, + HttpMethod verb = null, + object postData = null, Dictionary additionalHeaders = null, [EnumeratorCancellation] CancellationToken ctx = default) + { + var response = await HttpRequestRaw(url, verb, postData, streaming: true, additionalHeaders, 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/Extensions/ChatOptionsExtensions.cs b/Anthropic.SDK/Extensions/ChatOptionsExtensions.cs new file mode 100644 index 0000000..0e64ad6 --- /dev/null +++ b/Anthropic.SDK/Extensions/ChatOptionsExtensions.cs @@ -0,0 +1,117 @@ +using System; +using Microsoft.Extensions.AI; +using Anthropic.SDK.Messaging; + +namespace Anthropic.SDK.Extensions +{ + /// + /// Extensions for ChatOptions to support Anthropic-specific features + /// + public static class ChatOptionsExtensions + { + private const string ThinkingParametersKey = "Anthropic.ThinkingParameters"; + + /// + /// Sets thinking parameters for extended thinking support in compatible models like Claude 3.7 Sonnet + /// + /// The ChatOptions instance + /// The budget tokens for thinking (typically up to max_tokens unless using interleaved thinking) + /// The ChatOptions instance for fluent chaining + public static ChatOptions WithThinking(this ChatOptions options, int budgetTokens) + { + if (options == null) + throw new ArgumentNullException(nameof(options)); + + if (budgetTokens <= 0) + throw new ArgumentOutOfRangeException(nameof(budgetTokens), "Budget tokens must be greater than 0"); + + (options.AdditionalProperties ??= new())[ThinkingParametersKey] = new ThinkingParameters + { + BudgetTokens = budgetTokens + }; + + return options; + } + + /// + /// Sets thinking parameters for extended thinking support in compatible models like Claude 3.7 Sonnet + /// + /// The ChatOptions instance + /// The thinking parameters to set + /// The ChatOptions instance for fluent chaining + public static ChatOptions WithThinking(this ChatOptions options, ThinkingParameters thinkingParameters) + { + if (options == null) + throw new ArgumentNullException(nameof(options)); + + if (thinkingParameters == null) + throw new ArgumentNullException(nameof(thinkingParameters)); + + (options.AdditionalProperties ??= new())[ThinkingParametersKey] = thinkingParameters; + + return options; + } + + /// + /// Sets interleaved thinking parameters for enhanced thinking support. This enables the interleaved-thinking-2025-05-14 beta header + /// which allows thinking tokens to exceed max_tokens. Note: On 3rd-party platforms (Bedrock, Vertex AI), this only works + /// with Claude Opus 4.1, Opus 4, or Sonnet 4 models. + /// + /// The ChatOptions instance + /// The budget tokens for thinking (can exceed max_tokens when using interleaved thinking) + /// The ChatOptions instance for fluent chaining + public static ChatOptions WithInterleavedThinking(this ChatOptions options, int budgetTokens) + { + if (options == null) + throw new ArgumentNullException(nameof(options)); + + if (budgetTokens <= 0) + throw new ArgumentOutOfRangeException(nameof(budgetTokens), "Budget tokens must be greater than 0"); + + (options.AdditionalProperties ??= new())[ThinkingParametersKey] = new ThinkingParameters + { + BudgetTokens = budgetTokens, + UseInterleavedThinking = true + }; + + return options; + } + + /// + /// Sets interleaved thinking parameters for enhanced thinking support. This enables the interleaved-thinking-2025-05-14 beta header + /// which allows thinking tokens to exceed max_tokens. Note: On 3rd-party platforms (Bedrock, Vertex AI), this only works + /// with Claude Opus 4.1, Opus 4, or Sonnet 4 models. + /// + /// The ChatOptions instance + /// The thinking parameters to set + /// The ChatOptions instance for fluent chaining + public static ChatOptions WithInterleavedThinking(this ChatOptions options, ThinkingParameters thinkingParameters) + { + if (options == null) + throw new ArgumentNullException(nameof(options)); + + if (thinkingParameters == null) + throw new ArgumentNullException(nameof(thinkingParameters)); + + thinkingParameters.UseInterleavedThinking = true; + (options.AdditionalProperties ??= new())[ThinkingParametersKey] = thinkingParameters; + + return options; + } + + /// + /// Gets the thinking parameters from ChatOptions + /// + /// The ChatOptions instance + /// The thinking parameters, or null if not set + public static ThinkingParameters GetThinkingParameters(this ChatOptions options) + { + if (options?.AdditionalProperties?.TryGetValue(ThinkingParametersKey, out var value) == true) + { + return value as ThinkingParameters; + } + + return null; + } + } +} \ No newline at end of file diff --git a/Anthropic.SDK/Messaging/ChatClientHelper.cs b/Anthropic.SDK/Messaging/ChatClientHelper.cs index 479ab0f..1c4f59d 100644 --- a/Anthropic.SDK/Messaging/ChatClientHelper.cs +++ b/Anthropic.SDK/Messaging/ChatClientHelper.cs @@ -4,6 +4,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Anthropic.SDK.Common; +using Anthropic.SDK.Extensions; using Microsoft.Extensions.AI; namespace Anthropic.SDK.Messaging @@ -98,6 +99,13 @@ public static MessageParameters CreateMessageParameters(IChatClient client, IEnu } } } + + // Map thinking parameters from ChatOptions + var thinkingParameters = options.GetThinkingParameters(); + if (thinkingParameters != null) + { + parameters.Thinking = thinkingParameters; + } } foreach (ChatMessage message in messages) diff --git a/Anthropic.SDK/Messaging/MessagesEndpoint.cs b/Anthropic.SDK/Messaging/MessagesEndpoint.cs index 28f47c6..2ec5ef3 100644 --- a/Anthropic.SDK/Messaging/MessagesEndpoint.cs +++ b/Anthropic.SDK/Messaging/MessagesEndpoint.cs @@ -29,7 +29,30 @@ public async Task GetClaudeMessageAsync(MessageParameters param SetCacheControls(parameters); parameters.Stream = false; - var response = await HttpRequestMessages(Url, HttpMethod.Post, parameters, ctx).ConfigureAwait(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 response = await HttpRequestMessages(Url, HttpMethod.Post, parameters, additionalHeaders, ctx).ConfigureAwait(false); var toolCalls = new List(); foreach (var message in response.Content) @@ -94,12 +117,35 @@ public async IAsyncEnumerable StreamClaudeMessageAsync(MessageP SetCacheControls(parameters); 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 toolCalls = new List(); var arguments = string.Empty; var name = string.Empty; bool captureTool = false; var id = string.Empty; - await foreach (var result in HttpStreamingRequestMessages(Url, HttpMethod.Post, parameters, ctx).ConfigureAwait(false)) + await foreach (var result in HttpStreamingRequestMessages(Url, HttpMethod.Post, parameters, additionalHeaders, ctx).ConfigureAwait(false)) { if (result.ContentBlock != null && result.ContentBlock.Type == "tool_use") { diff --git a/Anthropic.SDK/Messaging/ThinkingParameters.cs b/Anthropic.SDK/Messaging/ThinkingParameters.cs index bfc786e..da334ab 100644 --- a/Anthropic.SDK/Messaging/ThinkingParameters.cs +++ b/Anthropic.SDK/Messaging/ThinkingParameters.cs @@ -12,5 +12,11 @@ public class ThinkingParameters public string Type => "enabled"; [JsonPropertyName("budget_tokens")] public int BudgetTokens { get; set; } + + /// + /// Indicates whether to use interleaved thinking mode which allows thinking tokens to exceed max_tokens + /// + [JsonIgnore] + public bool UseInterleavedThinking { get; set; } } } diff --git a/README.md b/README.md index 4722a0d..285fb76 100644 --- a/README.md +++ b/README.md @@ -216,13 +216,13 @@ messages.Add(new Message(RoleType.User, "How many r's are in the word strawberry var parameters = new MessageParameters() { Messages = messages, - MaxTokens = 20000, + MaxTokens = 4096, Model = AnthropicModels.Claude37Sonnet, Stream = false, Temperature = 1.0m, Thinking = new ThinkingParameters() { - BudgetTokens = 16000 + BudgetTokens = 4000 } }; var res = await client.Messages.GetClaudeMessageAsync(parameters); @@ -326,6 +326,88 @@ Assert.IsTrue(res.Message.Text?.Contains("apple", StringComparison.OrdinalIgnore ``` Please see the unit tests for even more examples. +#### Extended Thinking with IChatClient + +The `IChatClient` supports extended thinking through the ChatOptions extension methods. This provides a clean and fluent API for enabling thinking in compatible models like Claude 3.7 Sonnet: + +```csharp +using Anthropic.SDK.Extensions; + +IChatClient client = new AnthropicClient().Messages; + +List messages = new() +{ + new ChatMessage(ChatRole.User, "How many r's are in the word strawberry?") +}; + +// Using the extension method for thinking parameters +ChatOptions options = new() +{ + ModelId = AnthropicModels.Claude37Sonnet, + MaxOutputTokens = 4096, + Temperature = 1.0f, +}.WithThinking(4000); // Enable thinking with 4,000 budget tokens + +var res = await client.GetResponseAsync(messages, options); +Console.WriteLine(res.Text); // The final answer + +// Access thinking content if available +var thinkingContent = res.Message.Contents.OfType().FirstOrDefault(); +if (thinkingContent != null) +{ + Console.WriteLine("Claude's reasoning: " + thinkingContent.Text); +} + +// Continue the conversation +messages.AddMessages(res); +messages.Add(new ChatMessage(ChatRole.User, "And how many letters total?")); + +res = await client.GetResponseAsync(messages, options); +Console.WriteLine(res.Text); + +//Streaming with thinking +StringBuilder sb = new(); +await foreach (var update in client.GetStreamingResponseAsync(messages, options)) +{ + sb.Append(update); +} +Console.WriteLine(sb.ToString()); +``` + +You can also set thinking parameters using a `ThinkingParameters` object: + +```csharp +var thinkingParams = new ThinkingParameters { BudgetTokens = 3000 }; +ChatOptions options = new() +{ + ModelId = AnthropicModels.Claude37Sonnet, + MaxOutputTokens = 4096, + Temperature = 1.0f, +}.WithThinking(thinkingParams); +``` + +### Interleaved Thinking (Advanced) + +For enhanced thinking capabilities that allow thinking tokens to exceed max_tokens, you can use the `WithInterleavedThinking` extension method. This enables the `interleaved-thinking-2025-05-14` beta header: + +```csharp +// Enable interleaved thinking with budget tokens exceeding max_tokens +ChatOptions options = new() +{ + ModelId = AnthropicModels.Claude37Sonnet, + MaxOutputTokens = 4096, + Temperature = 1.0f, +}.WithInterleavedThinking(8000); // 8,000 thinking tokens with 4,096 max output tokens + +var response = await client.GetResponseAsync("Complex reasoning task", options); +``` + +**Important Notes about Interleaved Thinking:** +- On direct Anthropic API access, this feature works with all compatible models +- On 3rd-party platforms (Amazon Bedrock, Vertex AI), it only works with Claude Opus 4.1, Opus 4, or Sonnet 4 models +- With interleaved thinking enabled, thinking tokens can exceed the max_tokens limit +- Developers are responsible for ensuring compatibility with their chosen platform and model + ### Prompt Caching The `AnthropicClient` supports prompt caching of system messages, user messages (including images), assistant messages, tool_results, documents, and tools in accordance with model limitations. There are two primary mechanisms for prompt caching in the `AnthropicClient`. `FineGrained` and `AutomaticToolsAndSystem`. The former allows for complete control of all set-points of caching (up to the 4 set-points allowed) where-as `AutomaticToolsAndSystem` automatically caches the System prompt and Tools when present, leaving you the ability to add set-points in Messages yourself when you so choose. When caching, be aware of the 5 minute expiry enforced by Anthropic, as well as other limitations that can cause a cache miss. You can check the Token Usage data in results to ensure you are indeed receiving the benefits of caching.