diff --git a/Anthropic.SDK.Tests/Anthropic.SDK.Tests.csproj b/Anthropic.SDK.Tests/Anthropic.SDK.Tests.csproj index 3920640..b05f7e9 100644 --- a/Anthropic.SDK.Tests/Anthropic.SDK.Tests.csproj +++ b/Anthropic.SDK.Tests/Anthropic.SDK.Tests.csproj @@ -34,12 +34,12 @@ - + - - - + + + diff --git a/Anthropic.SDK.Tests/ChatOptionsExtensionsTests.cs b/Anthropic.SDK.Tests/ChatOptionsExtensionsTests.cs index eff3ebe..49b4cba 100644 --- a/Anthropic.SDK.Tests/ChatOptionsExtensionsTests.cs +++ b/Anthropic.SDK.Tests/ChatOptionsExtensionsTests.cs @@ -231,13 +231,13 @@ public void WithThinking_FluentChaining_Works() // Arrange & Act var options = new ChatOptions { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 4096, Temperature = 1.0f }.WithThinking(4000); // Assert - Assert.AreEqual(AnthropicModels.Claude37Sonnet, options.ModelId); + Assert.AreEqual(AnthropicModels.Claude46Sonnet, options.ModelId); Assert.AreEqual(4096, options.MaxOutputTokens); Assert.AreEqual(1.0f, options.Temperature); @@ -253,13 +253,13 @@ public void WithInterleavedThinking_FluentChaining_Works() // Arrange & Act var options = new ChatOptions { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 4096, Temperature = 1.0f }.WithInterleavedThinking(8000); // Assert - Assert.AreEqual(AnthropicModels.Claude37Sonnet, options.ModelId); + Assert.AreEqual(AnthropicModels.Claude46Sonnet, options.ModelId); Assert.AreEqual(4096, options.MaxOutputTokens); Assert.AreEqual(1.0f, options.Temperature); @@ -280,7 +280,7 @@ public void ChatClientHelper_MapsThinkingParametersCorrectly() }; var options = new ChatOptions { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 4096, Temperature = 1.0f }.WithThinking(3000); @@ -293,7 +293,7 @@ public void ChatClientHelper_MapsThinkingParametersCorrectly() Assert.AreEqual(3000, messageParams.Thinking.BudgetTokens); Assert.AreEqual(ThinkingType.enabled, messageParams.Thinking.Type); Assert.IsFalse(messageParams.Thinking.UseInterleavedThinking); - Assert.AreEqual(AnthropicModels.Claude37Sonnet, messageParams.Model); + Assert.AreEqual(AnthropicModels.Claude46Sonnet, messageParams.Model); Assert.AreEqual(4096, messageParams.MaxTokens); } @@ -308,7 +308,7 @@ public void ChatClientHelper_MapsInterleavedThinkingParametersCorrectly() }; var options = new ChatOptions { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 4096, Temperature = 1.0f }.WithInterleavedThinking(8000); @@ -321,7 +321,7 @@ public void ChatClientHelper_MapsInterleavedThinkingParametersCorrectly() Assert.AreEqual(8000, messageParams.Thinking.BudgetTokens); Assert.AreEqual(ThinkingType.enabled, messageParams.Thinking.Type); Assert.IsTrue(messageParams.Thinking.UseInterleavedThinking); - Assert.AreEqual(AnthropicModels.Claude37Sonnet, messageParams.Model); + Assert.AreEqual(AnthropicModels.Claude46Sonnet, messageParams.Model); Assert.AreEqual(4096, messageParams.MaxTokens); } diff --git a/Anthropic.SDK.Tests/CostTest.cs b/Anthropic.SDK.Tests/CostTest.cs new file mode 100644 index 0000000..502b040 --- /dev/null +++ b/Anthropic.SDK.Tests/CostTest.cs @@ -0,0 +1,38 @@ +using Anthropic.SDK.Constants; +using Anthropic.SDK.Messaging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Anthropic.SDK.Extensions; + +namespace Anthropic.SDK.Tests +{ + [TestClass] + public class CostTest + { + + [TestMethod] + public async Task TestCostEstimation() + { + var client = new AnthropicClient(); + var parameters = new MessageParameters() + { + Messages = new List { new Message(RoleType.User, "Hello!") }, + MaxTokens = 1024, + Model = AnthropicModels.Claude46Sonnet, + }; + var response = await client.Messages.GetClaudeMessageAsync(parameters); + + // Get total estimated cost + var cost = response.CalculateCost(); + Console.WriteLine($"Total cost: ${cost.TotalCostUsd:F6}"); + Console.WriteLine($" Input tokens: ${cost.InputTokenCost:F6}"); + Console.WriteLine($" Output tokens: ${cost.OutputTokenCost:F6}"); + Console.WriteLine($" Cache read: ${cost.CacheReadCost:F6}"); + Console.WriteLine($" Cache creation: ${cost.CacheCreationCost:F6}"); + Console.WriteLine($" Web search: ${cost.WebSearchCost:F6}"); + } + } +} diff --git a/Anthropic.SDK.Tests/MCPTests.cs b/Anthropic.SDK.Tests/MCPTests.cs index 4236e06..e693d64 100644 --- a/Anthropic.SDK.Tests/MCPTests.cs +++ b/Anthropic.SDK.Tests/MCPTests.cs @@ -18,15 +18,15 @@ public async Task TestMCP() var client = new AnthropicClient(); var parameters = new MessageParameters() { - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, MaxTokens = 5000, Temperature = 1, MCPServers = new List() { new MCPServer() { - Url = "https://mcp.deepwiki.com/sse", - Name = "DeepWiki", + Url = "https://learn.microsoft.com/api/mcp", + Name = "MSFT", } } }; @@ -37,7 +37,7 @@ public async Task TestMCP() Role = RoleType.User, Content = new List { - new TextContent { Text = "Tell me about the repo tghamm/Anthropic.SDK" } + new TextContent { Text = "Tell me about the Latest Microsoft.Extensions.AI Library" } } } }; @@ -55,7 +55,7 @@ public async Task TestMCPExtendedStreaming() var client = new AnthropicClient(); var parameters = new MessageParameters() { - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, MaxTokens = 3000, Temperature = 1, Stream = true, @@ -63,8 +63,8 @@ public async Task TestMCPExtendedStreaming() { new MCPServer() { - Url = "https://mcp.deepwiki.com/sse", - Name = "DeepWiki", + Url = "https://learn.microsoft.com/api/mcp", + Name = "MSFT", } } }; @@ -75,7 +75,7 @@ public async Task TestMCPExtendedStreaming() Role = RoleType.User, Content = new List { - new TextContent { Text = "Tell me about the repo tghamm/Anthropic.SDK" } + new TextContent { Text = "Tell me about the latest Microsoft.Extensions.AI Library" } } } }; diff --git a/Anthropic.SDK.Tests/Messages.ChatClient.cs b/Anthropic.SDK.Tests/Messages.ChatClient.cs index 1f124fb..52e3e78 100644 --- a/Anthropic.SDK.Tests/Messages.ChatClient.cs +++ b/Anthropic.SDK.Tests/Messages.ChatClient.cs @@ -91,7 +91,7 @@ public async Task TestNonStreamingThinkingWithExtensionMethods() ChatOptions options = new ChatOptions() { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 20000, Temperature = 1.0f, }.WithThinking(16000); @@ -166,7 +166,7 @@ public async Task TestThinkingStreamingWithExtensionMethods() ChatOptions options = new ChatOptions() { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 20000, Temperature = 1.0f, }.WithThinking(16000); @@ -210,7 +210,7 @@ public async Task TestNonStreamingThinkingConversation() ChatOptions options = new() { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 20000, Temperature = 1.0f, RawRepresentationFactory = static _ => new MessageParameters() @@ -239,7 +239,7 @@ public async Task TestThinkingStreamingConversation() ChatOptions options = new() { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 20000, Temperature = 1.0f, RawRepresentationFactory = static _ => new MessageParameters() @@ -287,7 +287,7 @@ public async Task TestThinkingStreamingRedactedConversation() ChatOptions options = new() { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 20000, Temperature = 1.0f, RawRepresentationFactory = static _ => new MessageParameters() @@ -357,7 +357,7 @@ public async Task TestNonStreamingThinkingFunctionCalls() ChatOptions options = new() { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 20000, Tools = [AIFunctionFactory.Create((string personName) => personName switch { "Alice" => "25", @@ -411,7 +411,7 @@ public async Task TestNonStreamingWebSearchCalls() ChatOptions options = new() { - ModelId = AnthropicModels.Claude4Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 5000, Tools = [new HostedWebSearchTool()] }; @@ -484,7 +484,7 @@ public async Task TestStreamingThinkingFunctionCalls() ChatOptions options = new() { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 20000, Tools = [AIFunctionFactory.Create((string personName) => personName switch { "Alice" => "25", @@ -551,16 +551,16 @@ public async Task TestNonStreamingMCPMessage() #pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. ChatOptions options = new() { - ModelId = AnthropicModels.Claude4Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 512, Temperature = 1.0f, - Tools = [new HostedMcpServerTool("DeepWiki", "https://mcp.deepwiki.com/sse")] + Tools = [new HostedMcpServerTool("MSFT", "https://learn.microsoft.com/api/mcp")] }; #pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - var res = await client.GetResponseAsync("Tell me about the repo tghamm/Anthropic.SDK", options); + var res = await client.GetResponseAsync("Tell me about the Latest Microsoft.Extensions.AI Library", options); - Assert.IsTrue(res.Text.ToLower().Contains("anthropic") is true, res.Text); + Assert.IsTrue(res.Text.ToLower().Contains("microsoft") is true, res.Text); } } } diff --git a/Anthropic.SDK.Tests/Messages.cs b/Anthropic.SDK.Tests/Messages.cs index 22acfa7..62db0ef 100644 --- a/Anthropic.SDK.Tests/Messages.cs +++ b/Anthropic.SDK.Tests/Messages.cs @@ -72,7 +72,7 @@ public async Task TestBasicClaude35HaikuMessage() { Messages = messages, MaxTokens = 512, - Model = AnthropicModels.Claude35Haiku, + Model = AnthropicModels.Claude45Haiku, Stream = false, Temperature = 1.0m, }; @@ -89,7 +89,7 @@ public async Task TestBasicTokenCountMessage() var parameters = new MessageCountTokenParameters { Messages = messages, - Model = AnthropicModels.Claude35Haiku + Model = AnthropicModels.Claude45Haiku }; var res = await client.Messages.CountMessageTokensAsync(parameters); Assert.IsTrue(res.InputTokens > 0); diff --git a/Anthropic.SDK.Tests/SemanticKernelInitializationTests.cs b/Anthropic.SDK.Tests/SemanticKernelInitializationTests.cs index cfc2537..52f3dc8 100644 --- a/Anthropic.SDK.Tests/SemanticKernelInitializationTests.cs +++ b/Anthropic.SDK.Tests/SemanticKernelInitializationTests.cs @@ -39,7 +39,7 @@ IChatClient CreateChatClient(IServiceProvider _) var skExecutionSettings = new OpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(), - ModelId = AnthropicModels.Claude35Haiku, + ModelId = AnthropicModels.Claude45Haiku, MaxTokens = 512 }; @@ -73,7 +73,7 @@ public async Task TestSKPDF() IChatCompletionService skChatService = new ChatClientBuilder(client.Messages) .ConfigureOptions(opt => { - opt.ModelId = AnthropicModels.Claude37Sonnet; + opt.ModelId = AnthropicModels.Claude46Sonnet; opt.MaxOutputTokens = 1024; }) .UseFunctionInvocation() @@ -116,7 +116,7 @@ public async Task TestSKLuckyNumber() OpenAIPromptExecutionSettings promptExecutionSettings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(), - ModelId = AnthropicModels.Claude35Haiku, + ModelId = AnthropicModels.Claude45Haiku, MaxTokens = 512 }; // Get the response from the AI @@ -147,7 +147,7 @@ public async Task TestSKLuckyNumberStreaming() OpenAIPromptExecutionSettings promptExecutionSettings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(), - ModelId = AnthropicModels.Claude35Haiku, + ModelId = AnthropicModels.Claude45Haiku, MaxTokens = 512 }; // Get the response from the AI diff --git a/Anthropic.SDK.Tests/ThinkingModeTests.cs b/Anthropic.SDK.Tests/ThinkingModeTests.cs index 81429ab..b6ca376 100644 --- a/Anthropic.SDK.Tests/ThinkingModeTests.cs +++ b/Anthropic.SDK.Tests/ThinkingModeTests.cs @@ -27,7 +27,7 @@ public async Task TestBasicClaude37ThinkingMessage() { Messages = messages, MaxTokens = 20000, - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, Thinking = new ThinkingParameters() @@ -81,7 +81,7 @@ public async Task TestRedactedClaude37ThinkingMessage() { Messages = messages, MaxTokens = 20000, - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, Thinking = new ThinkingParameters() @@ -105,7 +105,7 @@ public async Task TestClaude37ThinkingConversation() { Messages = messages, MaxTokens = 20000, - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, Thinking = new ThinkingParameters() @@ -138,7 +138,7 @@ public async Task TestStreamingClaude37SonnetThinkingConversation() { Messages = messages, MaxTokens = 20000, - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = true, Temperature = 1.0m, Thinking = new ThinkingParameters() @@ -184,7 +184,7 @@ public async Task TestStreamingRedactedClaude37SonnetThinkingConversation() { Messages = messages, MaxTokens = 20000, - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = true, Temperature = 1.0m, Thinking = new ThinkingParameters() @@ -303,7 +303,7 @@ public async Task TestBasicClaude37ImageStreamingSchemaMessage() { Messages = messages, MaxTokens = 20000, - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = true, Temperature = 1.0m, Tools = tools.ToList(), diff --git a/Anthropic.SDK.Tests/VertexAI.ChatClient.cs b/Anthropic.SDK.Tests/VertexAI.ChatClient.cs index a62d2d4..e5b800b 100644 --- a/Anthropic.SDK.Tests/VertexAI.ChatClient.cs +++ b/Anthropic.SDK.Tests/VertexAI.ChatClient.cs @@ -22,7 +22,7 @@ public async Task TestNonStreamingMessage() ChatOptions options = new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 512, Temperature = 1.0f, }; @@ -44,7 +44,7 @@ public async Task TestNonStreamingConversation() ChatOptions options = new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 20000, Temperature = 1.0f, }; @@ -70,7 +70,7 @@ public async Task TestStreamingConversation() ChatOptions options = new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 20000, Temperature = 1.0f, }; @@ -111,7 +111,7 @@ public async Task TestNonStreamingThinkingConversation() ChatOptions options = new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 20000, Temperature = 1.0f, RawRepresentationFactory = static _ => new MessageParameters() @@ -141,7 +141,7 @@ public async Task TestThinkingStreamingConversation() ChatOptions options = new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 20000, Temperature = 1.0f, RawRepresentationFactory = static _ => new MessageParameters() @@ -187,7 +187,7 @@ public async Task TestNonStreamingFunctionCalls() ChatOptions options = new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 512, Tools = [AIFunctionFactory.Create((string personName) => personName switch { "Alice" => "25", @@ -212,7 +212,7 @@ public async Task TestStreamingFunctionCalls() ChatOptions options = new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 512, Tools = [AIFunctionFactory.Create((string personName) => personName switch { "Alice" => "25", @@ -243,7 +243,7 @@ public async Task TestThinkingStreamingRedactedConversation() ChatOptions options = new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 20000, Temperature = 1.0f, RawRepresentationFactory = static _ => new MessageParameters() @@ -283,7 +283,7 @@ public async Task TestStreamingMessage() ChatOptions options = new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 512, Temperature = 1.0f, }; @@ -313,7 +313,7 @@ public async Task TestNonStreamingThinkingFunctionCalls() ChatOptions options = new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 20000, Tools = [AIFunctionFactory.Create((string personName) => personName switch { "Alice" => "25", @@ -342,7 +342,7 @@ public async Task TestStreamingThinkingFunctionCalls() ChatOptions options = new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 20000, Tools = [AIFunctionFactory.Create((string personName) => personName switch { "Alice" => "25", @@ -390,7 +390,7 @@ public async Task TestVertexAIImageMessage() ]) ], new() { - ModelId = Constants.VertexAIModels.Claude37Sonnet, + ModelId = Constants.VertexAIModels.Claude46Sonnet, MaxOutputTokens = 512, Temperature = 0f, }); diff --git a/Anthropic.SDK.Tests/VertexAI.cs b/Anthropic.SDK.Tests/VertexAI.cs index 945500f..524f0a8 100644 --- a/Anthropic.SDK.Tests/VertexAI.cs +++ b/Anthropic.SDK.Tests/VertexAI.cs @@ -24,7 +24,7 @@ public async Task TestBasicVertexAIMessage() { Messages = messages, MaxTokens = 512, - Model = Constants.VertexAIModels.Claude37Sonnet, + Model = Constants.VertexAIModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, }; @@ -51,7 +51,7 @@ public async Task TestVertexAIWithModelSelection() MaxTokens = 512, Stream = false, Temperature = 1.0m, - Model = Constants.VertexAIModels.Claude37Sonnet + Model = Constants.VertexAIModels.Claude46Sonnet }; var res = await client.Messages.GetClaudeMessageAsync(parameters); @@ -74,7 +74,7 @@ public async Task TestStreamingVertexAIMessage() { Messages = messages, MaxTokens = 512, - Model = Constants.VertexAIModels.Claude37Sonnet, + Model = Constants.VertexAIModels.Claude46Sonnet, Stream = true, Temperature = 1.0m, }; @@ -142,7 +142,7 @@ public async Task TestVertexAIImageMessage() { Messages = messages, MaxTokens = 512, - Model = Constants.VertexAIModels.Claude37Sonnet, + Model = Constants.VertexAIModels.Claude46Sonnet, Stream = false, Temperature = 0.0m, // Use deterministic output for testing }; @@ -199,7 +199,7 @@ public async Task TestStreamingVertexAIImageMessage() { Messages = messages, MaxTokens = 512, - Model = Constants.VertexAIModels.Claude37Sonnet, + Model = Constants.VertexAIModels.Claude46Sonnet, Stream = true, Temperature = 0.0m, // Use deterministic output for testing }; @@ -238,7 +238,7 @@ public async Task TestVertexAIWithSingleSystemPrompt() Messages = messages, System = new List { systemPrompt }, MaxTokens = 512, - Model = Constants.VertexAIModels.Claude37Sonnet, + Model = Constants.VertexAIModels.Claude46Sonnet, Stream = false, Temperature = 0.7m, }; @@ -280,7 +280,7 @@ public async Task TestVertexAIWithMultipleSystemPrompts() Messages = messages, System = systemPrompts, MaxTokens = 512, - Model = Constants.VertexAIModels.Claude37Sonnet, + Model = Constants.VertexAIModels.Claude46Sonnet, Stream = false, Temperature = 0.7m, }; @@ -323,7 +323,7 @@ public async Task TestVertexAIWithSystemPromptCacheControl() Messages = messages, System = systemPrompts, MaxTokens = 512, - Model = Constants.VertexAIModels.Claude37Sonnet, + Model = Constants.VertexAIModels.Claude46Sonnet, Stream = false, Temperature = 0.7m, PromptCaching = PromptCacheType.FineGrained // Use fine-grained caching since we're setting cache control explicitly diff --git a/Anthropic.SDK.Tests/VisionTests.cs b/Anthropic.SDK.Tests/VisionTests.cs index d47da62..4ced763 100644 --- a/Anthropic.SDK.Tests/VisionTests.cs +++ b/Anthropic.SDK.Tests/VisionTests.cs @@ -18,7 +18,7 @@ public async Task TestVisionUrl() var mp = new MessageParameters() { - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, MaxTokens = 1024, Messages = new List() { diff --git a/Anthropic.SDK.Tests/WebSearchFunctionality.cs b/Anthropic.SDK.Tests/WebSearchFunctionality.cs index e767742..401a838 100644 --- a/Anthropic.SDK.Tests/WebSearchFunctionality.cs +++ b/Anthropic.SDK.Tests/WebSearchFunctionality.cs @@ -1,12 +1,15 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; using Anthropic.SDK.Constants; +using Anthropic.SDK.Extensions; using Anthropic.SDK.Messaging; +using Microsoft.Extensions.AI; using Tool = Anthropic.SDK.Common.Tool; +using TextContent = Anthropic.SDK.Messaging.TextContent; namespace Anthropic.SDK.Tests { @@ -19,7 +22,7 @@ public async Task TestWebSearch() var client = new AnthropicClient(); var parameters = new MessageParameters() { - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, MaxTokens = 3000, Temperature = 1, Tools = new List(){ServerTools.GetWebSearchTool()}, @@ -53,7 +56,7 @@ public async Task TestWebSearchExtended() var client = new AnthropicClient(); var parameters = new MessageParameters() { - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, MaxTokens = 3000, Temperature = 1, Tools = new List() @@ -101,7 +104,7 @@ public async Task TestWebSearchExtendedStreaming() var client = new AnthropicClient(); var parameters = new MessageParameters() { - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, MaxTokens = 3000, Temperature = 1, Stream = true, @@ -158,5 +161,240 @@ public async Task TestWebSearchExtendedStreaming() Console.WriteLine("Final Result:"); Console.WriteLine(textResult.Last().Text); } + + [TestMethod] + public async Task TestWebSearchDynamicFiltering() + { + var client = new AnthropicClient(); + var parameters = new MessageParameters() + { + Model = AnthropicModels.Claude46Sonnet, + MaxTokens = 4096, + Temperature = 1, + Tools = new List() + { + ServerTools.GetWebSearchTool() + }, + ToolChoice = new ToolChoice() + { + Type = ToolChoiceType.Auto + }, + }; + var messages = new List + { + new Message + { + Role = RoleType.User, + Content = new List + { + new TextContent { Text = "Search for the current population of Tokyo and give me the number." } + } + } + }; + parameters.Messages = messages; + var res = await client.Messages.GetClaudeMessageAsync(parameters); + + Assert.IsNotNull(res); + Assert.IsTrue(res.Content.OfType().Any()); + Console.WriteLine("----------------------------------------------"); + Console.WriteLine("Dynamic Filtering Result:"); + Console.WriteLine(res.Content.OfType().Last().Text); + } + + [TestMethod] + public async Task TestWebFetch() + { + var client = new AnthropicClient(); + var parameters = new MessageParameters() + { + Model = AnthropicModels.Claude46Sonnet, + MaxTokens = 4096, + Temperature = 1, + Tools = new List() + { + ServerTools.GetWebFetchTool(maxUses: 5, enableCitations: true, + toolVersion: ServerTools.WebFetchVersionLegacy) + }, + ToolChoice = new ToolChoice() + { + Type = ToolChoiceType.Auto + }, + }; + var messages = new List + { + new Message + { + Role = RoleType.User, + Content = new List + { + new TextContent { Text = "Fetch the content at https://www.wikipedia.org and tell me what it says." } + } + } + }; + parameters.Messages = messages; + var res = await client.Messages.GetClaudeMessageAsync(parameters); + + Assert.IsNotNull(res); + Assert.IsTrue(res.Content.OfType().Any()); + Console.WriteLine("----------------------------------------------"); + Console.WriteLine("Web Fetch Result:"); + Console.WriteLine(res.Content.OfType().Last().Text); + } + + [TestMethod] + public async Task TestWebFetchStreaming() + { + var client = new AnthropicClient(); + var parameters = new MessageParameters() + { + Model = AnthropicModels.Claude46Sonnet, + MaxTokens = 4096, + Temperature = 1, + Stream = true, + Tools = new List() + { + ServerTools.GetWebFetchTool(maxUses: 5, + toolVersion: ServerTools.WebFetchVersionLegacy) + }, + ToolChoice = new ToolChoice() + { + Type = ToolChoiceType.Auto + }, + }; + var messages = new List + { + new Message + { + Role = RoleType.User, + Content = new List + { + new TextContent { Text = "Fetch the content at https://example.com and summarize it." } + } + } + }; + parameters.Messages = messages; + var outputs = new List(); + await foreach (var res in client.Messages.StreamClaudeMessageAsync(parameters)) + { + if (res.Delta != null) + { + Debug.Write(res.Delta.Text); + } + + outputs.Add(res); + } + + var message = new Message(outputs); + Assert.IsNotNull(message); + Assert.IsTrue(message.Content.OfType().Any()); + Console.WriteLine("----------------------------------------------"); + Console.WriteLine("Web Fetch Streaming Result:"); + Console.WriteLine(message.Content.OfType().Last().Text); + } + + [TestMethod] + public async Task TestWebSearchWithLegacyVersion() + { + var client = new AnthropicClient(); + var parameters = new MessageParameters() + { + Model = AnthropicModels.Claude46Sonnet, + MaxTokens = 3000, + Temperature = 1, + Tools = new List() + { + ServerTools.GetWebSearchTool(toolVersion: ServerTools.WebSearchVersionLegacy) + }, + ToolChoice = new ToolChoice() + { + Type = ToolChoiceType.Auto + }, + }; + var messages = new List + { + new Message + { + Role = RoleType.User, + Content = new List + { + new TextContent { Text = "What is the weather like in San Francisco right now?" } + } + } + }; + parameters.Messages = messages; + var res = await client.Messages.GetClaudeMessageAsync(parameters); + + Assert.IsNotNull(res); + Assert.IsTrue(res.Content.OfType().Any()); + Console.WriteLine("----------------------------------------------"); + Console.WriteLine("Legacy Web Search Result:"); + Console.WriteLine(res.Content.OfType().Last().Text); + } + + [TestMethod] + public async Task TestNonStreamingWebFetchChatClient() + { + IChatClient client = new AnthropicClient().Messages + .AsBuilder() + .UseFunctionInvocation() + .Build(); + + ChatOptions options = new ChatOptions() + { + ModelId = AnthropicModels.Claude46Sonnet, + MaxOutputTokens = 4096, + }.WithWebFetch(maxUses: 5, enableCitations: true); + + var res = await client.GetResponseAsync( + "Fetch the content at https://example.com and tell me what it says.", options); + + Assert.IsNotNull(res); + Assert.IsTrue(!string.IsNullOrEmpty(res.Text)); + Console.WriteLine("----------------------------------------------"); + Console.WriteLine("IChatClient Web Fetch Result:"); + Console.WriteLine(res.Text); + } + + [TestMethod] + public async Task TestCombinedWebSearchAndFetch() + { + var client = new AnthropicClient(); + var parameters = new MessageParameters() + { + Model = AnthropicModels.Claude46Sonnet, + MaxTokens = 4096, + Temperature = 1, + Tools = new List() + { + ServerTools.GetWebSearchTool(maxUses: 3, + toolVersion: ServerTools.WebSearchVersionLegacy), + ServerTools.GetWebFetchTool(maxUses: 5, enableCitations: true, + toolVersion: ServerTools.WebFetchVersionLegacy) + }, + ToolChoice = new ToolChoice() + { + Type = ToolChoiceType.Auto + }, + }; + var messages = new List + { + new Message + { + Role = RoleType.User, + Content = new List + { + new TextContent { Text = "Search for the Anthropic homepage and then fetch its content to summarize it." } + } + } + }; + parameters.Messages = messages; + var res = await client.Messages.GetClaudeMessageAsync(parameters); + + Assert.IsNotNull(res); + Assert.IsTrue(res.Content.OfType().Any()); + Console.WriteLine("----------------------------------------------"); + Console.WriteLine("Combined Search + Fetch Result:"); + Console.WriteLine(res.Content.OfType().Last().Text); + } } } diff --git a/Anthropic.SDK/Anthropic.SDK.csproj b/Anthropic.SDK/Anthropic.SDK.csproj index 57d22c9..6e1c308 100644 --- a/Anthropic.SDK/Anthropic.SDK.csproj +++ b/Anthropic.SDK/Anthropic.SDK.csproj @@ -14,12 +14,12 @@ Claude, AI, ML, API, Anthropic Claude API - ModelContextProtocol Integration, Various Upgrades, .Net 10 Support, Bug Fixes, Latest M.E.AI. + Opus/Sonnet 4.6 Series, Adaptive Thinking, Costing, Web Search/Fetch, Bug Fixes, Latest M.E.AI. Anthropic.SDK - 5.9.0 + 5.10.0 5.5.0.0 - 5.9.0.0 + 5.10.0.0 True README.md icon.png diff --git a/Anthropic.SDK/Constants/AnthropicModels.cs b/Anthropic.SDK/Constants/AnthropicModels.cs index f3ae820..19fe5fb 100644 --- a/Anthropic.SDK/Constants/AnthropicModels.cs +++ b/Anthropic.SDK/Constants/AnthropicModels.cs @@ -24,11 +24,6 @@ public static class AnthropicModels /// public const string Claude45Opus = "claude-opus-4-5-20251101"; - /// - /// Claude 3.7 Sonnet - /// - public const string Claude37Sonnet = "claude-3-7-sonnet-20250219"; - /// /// Claude 4 Sonnet /// @@ -49,11 +44,6 @@ public static class AnthropicModels /// public const string Claude41Opus = "claude-opus-4-1-20250805"; - /// - /// Claude 3.5 Haiku - /// - public const string Claude35Haiku = "claude-3-5-haiku-20241022"; - /// /// Claude 4.5 Haiku /// diff --git a/Anthropic.SDK/Constants/VertexAIModels.cs b/Anthropic.SDK/Constants/VertexAIModels.cs index a79bd61..e19be34 100644 --- a/Anthropic.SDK/Constants/VertexAIModels.cs +++ b/Anthropic.SDK/Constants/VertexAIModels.cs @@ -12,11 +12,7 @@ public static class VertexAIModels public const string Claude3Haiku = "claude-3-haiku@20240307"; - /// - /// Claude 3.7 Sonnet on Vertex AI - Highest level of intelligence and capability with toggleable extended thinking - /// - public const string Claude37Sonnet = "claude-3-7-sonnet@20250219"; - + /// /// Claude 4 Sonnet on Vertex AI /// diff --git a/Anthropic.SDK/Extensions/ChatOptionsExtensions.cs b/Anthropic.SDK/Extensions/ChatOptionsExtensions.cs index f0dd699..269e57f 100644 --- a/Anthropic.SDK/Extensions/ChatOptionsExtensions.cs +++ b/Anthropic.SDK/Extensions/ChatOptionsExtensions.cs @@ -1,9 +1,22 @@ using System; +using System.Collections.Generic; using Microsoft.Extensions.AI; using Anthropic.SDK.Messaging; namespace Anthropic.SDK.Extensions { + /// + /// Configuration for the web fetch tool when used via IChatClient. + /// + public class WebFetchConfiguration + { + public int? MaxUses { get; set; } + public List AllowedDomains { get; set; } + public List BlockedDomains { get; set; } + public bool EnableCitations { get; set; } + public int? MaxContentTokens { get; set; } + } + /// /// Extensions for ChatOptions to support Anthropic-specific features /// @@ -11,6 +24,7 @@ public static class ChatOptionsExtensions { private const string ThinkingParametersKey = "Anthropic.ThinkingParameters"; private const string StrictToolsKey = "Anthropic.StrictTools"; + private const string WebFetchKey = "Anthropic.WebFetch"; /// /// Sets thinking parameters for extended thinking support in compatible models like Claude 3.7 Sonnet @@ -187,5 +201,54 @@ public static bool GetStrictToolsEnabled(this ChatOptions options) return false; } + + /// + /// Enables the web fetch tool for IChatClient requests. Since Microsoft.Extensions.AI does not + /// have a built-in HostedWebFetchTool, this extension method provides an Anthropic-specific way + /// to enable web fetch capability. + /// + /// The ChatOptions instance + /// Optional limit on the number of fetches per request + /// Optional list of domains to allow fetching from + /// Optional list of domains to block fetching from + /// Whether to enable citations for fetched content + /// Optional maximum content length in tokens + /// The ChatOptions instance for fluent chaining + public static ChatOptions WithWebFetch(this ChatOptions options, + int? maxUses = null, + List allowedDomains = null, + List blockedDomains = null, + bool enableCitations = false, + int? maxContentTokens = null) + { + if (options == null) + throw new ArgumentNullException(nameof(options)); + + (options.AdditionalProperties ??= new())[WebFetchKey] = new WebFetchConfiguration + { + MaxUses = maxUses, + AllowedDomains = allowedDomains, + BlockedDomains = blockedDomains, + EnableCitations = enableCitations, + MaxContentTokens = maxContentTokens + }; + + return options; + } + + /// + /// Gets the web fetch configuration from ChatOptions, if set. + /// + /// The ChatOptions instance + /// The web fetch configuration, or null if not set + public static WebFetchConfiguration GetWebFetchConfiguration(this ChatOptions options) + { + if (options?.AdditionalProperties?.TryGetValue(WebFetchKey, out var value) == true) + { + return value as WebFetchConfiguration; + } + + return null; + } } } \ No newline at end of file diff --git a/Anthropic.SDK/Extensions/ContentConverter.cs b/Anthropic.SDK/Extensions/ContentConverter.cs index 7e659cd..0c89a9a 100644 --- a/Anthropic.SDK/Extensions/ContentConverter.cs +++ b/Anthropic.SDK/Extensions/ContentConverter.cs @@ -1,4 +1,4 @@ -using Anthropic.SDK.Messaging; +using Anthropic.SDK.Messaging; using System; using System.Text.Json.Serialization; using System.Text.Json; @@ -44,10 +44,20 @@ public override ContentBase Read(ref Utf8JsonReader reader, Type typeToConvert, return JsonSerializer.Deserialize(root.GetRawText(), options); case "web_search_tool_result_error": return JsonSerializer.Deserialize(root.GetRawText(), options); + case "web_fetch_tool_result": + return JsonSerializer.Deserialize(root.GetRawText(), options); + case "web_fetch_result": + return JsonSerializer.Deserialize(root.GetRawText(), options); + case "web_fetch_tool_error": + return JsonSerializer.Deserialize(root.GetRawText(), options); case "mcp_tool_use": return JsonSerializer.Deserialize(root.GetRawText(), options); case "mcp_tool_result": return JsonSerializer.Deserialize(root.GetRawText(), options); + case "code_execution_tool_result": + return JsonSerializer.Deserialize(root.GetRawText(), options); + case "code_execution_tool_result_error": + return JsonSerializer.Deserialize(root.GetRawText(), options); case "bash_code_execution_tool_result": return JsonSerializer.Deserialize(root.GetRawText(), options); case "bash_code_execution_result": @@ -81,6 +91,12 @@ public override ContentBase Read(ref Utf8JsonReader reader, Type typeToConvert, public override void Write(Utf8JsonWriter writer, ContentBase value, JsonSerializerOptions options) { + if (value is UnknownContent unknown && !string.IsNullOrEmpty(unknown.RawJson)) + { + using var doc = JsonDocument.Parse(unknown.RawJson); + doc.RootElement.WriteTo(writer); + return; + } JsonSerializer.Serialize(writer, value, value.GetType(), options); } } diff --git a/Anthropic.SDK/Extensions/CostCalculationExtensions.cs b/Anthropic.SDK/Extensions/CostCalculationExtensions.cs new file mode 100644 index 0000000..8f652f0 --- /dev/null +++ b/Anthropic.SDK/Extensions/CostCalculationExtensions.cs @@ -0,0 +1,162 @@ +using System; +using Anthropic.SDK.Messaging; + +namespace Anthropic.SDK.Extensions +{ + /// + /// Detailed breakdown of estimated costs for an API request. + /// All values are in USD. + /// + public class CostBreakdown + { + /// + /// Cost of base input tokens. + /// + public decimal InputTokenCost { get; set; } + + /// + /// Cost of output tokens. + /// + public decimal OutputTokenCost { get; set; } + + /// + /// Cost of cache read tokens. + /// + public decimal CacheReadCost { get; set; } + + /// + /// Cost of cache creation tokens (combined 5-minute and 1-hour). + /// When detailed cache creation breakdown is unavailable, the legacy + /// cache_creation_input_tokens field is priced at the 5-minute write rate. + /// + public decimal CacheCreationCost { get; set; } + + /// + /// Cost of web search requests ($0.01 per search). + /// + public decimal WebSearchCost { get; set; } + + /// + /// Total estimated cost in USD (sum of all components). + /// + public decimal TotalCostUsd => + InputTokenCost + OutputTokenCost + CacheReadCost + CacheCreationCost + WebSearchCost; + + /// + /// The used for this calculation. + /// + public ModelPricing Pricing { get; set; } + } + + /// + /// Extension methods for calculating estimated API costs from usage data. + /// + public static class CostCalculationExtensions + { + private const decimal PerMillionDivisor = 1_000_000m; + private const decimal Per1000Divisor = 1_000m; + + /// + /// Calculate the estimated cost of an API request from its data. + /// When the service tier is , a 50% discount is applied + /// to all token costs automatically. + /// + /// The usage data from the API response. + /// The model ID string used for the request. + /// + /// Optional pricing to use instead of the built-in/registered pricing. + /// + /// A with per-category costs. + /// Thrown when is null. + /// + /// Thrown when no pricing can be found for + /// and is not provided. + /// + public static CostBreakdown CalculateCost( + this Usage usage, + string modelId, + ModelPricing overridePricing = null) + { + if (usage == null) + throw new ArgumentNullException(nameof(usage)); + + var pricing = overridePricing ?? ModelPricing.ForModel(modelId); + if (pricing == null) + { + throw new InvalidOperationException( + $"No pricing found for model '{modelId}'. " + + "Use ModelPricing.Register() to add pricing, or pass overridePricing."); + } + + decimal batchMultiplier = usage.ServiceTier == ServiceTier.Batch ? 0.5m : 1m; + + decimal inputCost = usage.InputTokens / PerMillionDivisor + * pricing.InputTokenCostPerMillion * batchMultiplier; + + decimal outputCost = usage.OutputTokens / PerMillionDivisor + * pricing.OutputTokenCostPerMillion * batchMultiplier; + + decimal cacheReadCost = usage.CacheReadInputTokens / PerMillionDivisor + * pricing.CacheReadCostPerMillion * batchMultiplier; + + decimal cacheCreationCost = 0m; + + if (usage.CacheCreation != null) + { + int tokens5m = usage.CacheCreation.Ephemeral5mInputTokens ?? 0; + int tokens1h = usage.CacheCreation.Ephemeral1hInputTokens ?? 0; + + cacheCreationCost = + (tokens5m / PerMillionDivisor * pricing.Cache5mWriteCostPerMillion * batchMultiplier) + + (tokens1h / PerMillionDivisor * pricing.Cache1hWriteCostPerMillion * batchMultiplier); + } + + if (usage.CacheCreationInputTokens > 0 && cacheCreationCost == 0m) + { + cacheCreationCost = usage.CacheCreationInputTokens / PerMillionDivisor + * pricing.Cache5mWriteCostPerMillion * batchMultiplier; + } + + decimal webSearchCost = 0m; + if (usage.ServerToolUse?.WebSearchRequests is > 0) + { + webSearchCost = usage.ServerToolUse.WebSearchRequests.Value / Per1000Divisor + * pricing.WebSearchCostPer1000; + } + + return new CostBreakdown + { + InputTokenCost = inputCost, + OutputTokenCost = outputCost, + CacheReadCost = cacheReadCost, + CacheCreationCost = cacheCreationCost, + WebSearchCost = webSearchCost, + Pricing = pricing, + }; + } + + /// + /// Calculate the estimated cost of an API request directly from the . + /// Uses the model from the response and its usage data. + /// + /// The message response from the API. + /// + /// Optional pricing to use instead of the built-in/registered pricing. + /// + /// A with per-category costs. + /// + /// Thrown when or its Usage is null. + /// + public static CostBreakdown CalculateCost( + this MessageResponse response, + ModelPricing overridePricing = null) + { + if (response == null) + throw new ArgumentNullException(nameof(response)); + if (response.Usage == null) + throw new ArgumentNullException(nameof(response), "Response.Usage is null."); + + return response.Usage.CalculateCost(response.Model, overridePricing); + } + } +} diff --git a/Anthropic.SDK/Messaging/ChatClientHelper.cs b/Anthropic.SDK/Messaging/ChatClientHelper.cs index 09f980f..9f78667 100644 --- a/Anthropic.SDK/Messaging/ChatClientHelper.cs +++ b/Anthropic.SDK/Messaging/ChatClientHelper.cs @@ -112,7 +112,7 @@ public static MessageParameters CreateMessageParameters(IChatClient client, IEnu tools.Add(ServerTools.GetCodeExecutionTool()); break; - case HostedWebSearchTool: + case HostedWebSearchTool hwst: tools.Add(ServerTools.GetWebSearchTool(5)); break; @@ -138,6 +138,19 @@ public static MessageParameters CreateMessageParameters(IChatClient client, IEnu } } + // Map web fetch configuration from ChatOptions extension + var webFetchConfig = options.GetWebFetchConfiguration(); + if (webFetchConfig != null) + { + IList tools = parameters.Tools ??= []; + tools.Add(ServerTools.GetWebFetchTool( + maxUses: webFetchConfig.MaxUses, + allowedDomains: webFetchConfig.AllowedDomains, + blockedDomains: webFetchConfig.BlockedDomains, + enableCitations: webFetchConfig.EnableCitations, + maxContentTokens: webFetchConfig.MaxContentTokens)); + } + // Map thinking parameters from ChatOptions var thinkingParameters = options.GetThinkingParameters(); if (thinkingParameters != null) @@ -229,7 +242,12 @@ public static MessageParameters CreateMessageParameters(IChatClient client, IEnu currentMessage.Content.Add(new ToolResultContent() { ToolUseId = frc.CallId, - Content = new List() { new TextContent () { Text = frc.Result?.ToString() ?? string.Empty } }, + Content = new List() { new TextContent () { Text = frc.Result switch + { + string s => s, + null => string.Empty, + _ => JsonSerializer.Serialize(frc.Result) + } } }, IsError = frc.Exception is not null, }); break; @@ -375,6 +393,34 @@ public static List ProcessResponseContent(MessageResponse response) trc.ToolUseId, trc.Content)); break; + + case WebSearchToolResultContent wsResult: + var wsText = new Microsoft.Extensions.AI.TextContent($"[Web Search Results: {wsResult.Content?.Count ?? 0} results]") + { + RawRepresentation = wsResult + }; + if (wsResult.Content != null) + { + wsText.Annotations ??= new List(); + foreach (var item in wsResult.Content.OfType()) + { + wsText.Annotations.Add(new CitationAnnotation + { + RawRepresentation = item, + FileId = item.Title + }); + } + } + contents.Add(wsText); + break; + + case WebFetchToolResultContent wfResult: + var wfText = new Microsoft.Extensions.AI.TextContent("[Web Fetch Result]") + { + RawRepresentation = wfResult + }; + contents.Add(wfText); + break; } } diff --git a/Anthropic.SDK/Messaging/Content.cs b/Anthropic.SDK/Messaging/Content.cs index 59ca596..741061f 100644 --- a/Anthropic.SDK/Messaging/Content.cs +++ b/Anthropic.SDK/Messaging/Content.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using System.Text.Json.Nodes; @@ -85,6 +85,12 @@ public class ServerToolInput /// [JsonPropertyName("file_text")] public string FileText { get; set; } + + /// + /// URL for web_fetch tool + /// + [JsonPropertyName("url")] + public string Url { get; set; } } @@ -527,6 +533,33 @@ public class WebSearchResultContent : ContentBase public string PageAge { get; set; } } + /// + /// Code Execution Tool Result Content (used by dynamic filtering web search/fetch) + /// + public class CodeExecutionToolResultContent : ContentBase + { + [JsonPropertyName("type")] + public override ContentType Type => ContentType.code_execution_tool_result; + + [JsonPropertyName("tool_use_id")] + public string ToolUseId { get; set; } + + [JsonPropertyName("content")] + public ContentBase Content { get; set; } + } + + /// + /// Code Execution Tool Result Error Content + /// + public class CodeExecutionToolResultErrorContent : ContentBase + { + [JsonPropertyName("type")] + public override ContentType Type => ContentType.code_execution_tool_result_error; + + [JsonPropertyName("error_code")] + public string ErrorCode { get; set; } + } + /// /// Bash Code Execution Tool Result Content /// @@ -851,6 +884,69 @@ public class TextEditorCodeExecutionStrReplaceResultContent : ContentBase public int? OldStart { get; set; } } + /// + /// Web Fetch Tool Result Content Returned From Claude + /// + public class WebFetchToolResultContent : ContentBase + { + /// + /// Type of Content (web_fetch_tool_result, pre-set) + /// + [JsonPropertyName("type")] + public override ContentType Type => ContentType.web_fetch_tool_result; + + /// + /// Tool Use Id + /// + [JsonPropertyName("tool_use_id")] + public string ToolUseId { get; set; } + + /// + /// Content of the Tool Result + /// + [JsonPropertyName("content")] + public ContentBase Content { get; set; } + } + + /// + /// Web Fetch Result Content containing the fetched document + /// + public class WebFetchResultContent : ContentBase + { + /// + /// Type of Content (web_fetch_result, pre-set) + /// + [JsonPropertyName("type")] + public override ContentType Type => ContentType.web_fetch_result; + + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("content")] + public ContentBase Content { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("retrieved_at")] + public string RetrievedAt { get; set; } + } + + /// + /// Web Fetch Tool Error Content + /// + public class WebFetchToolErrorContent : ContentBase + { + /// + /// Type of Content (web_fetch_tool_error, pre-set) + /// + [JsonPropertyName("type")] + public override ContentType Type => ContentType.web_fetch_tool_error; + + [JsonPropertyName("error_code")] + public string ErrorCode { get; set; } + } + /// /// Unknown content type - used as fallback for forward compatibility when the API returns new content types /// diff --git a/Anthropic.SDK/Messaging/ContentType.cs b/Anthropic.SDK/Messaging/ContentType.cs index 7e60ae2..671ff91 100644 --- a/Anthropic.SDK/Messaging/ContentType.cs +++ b/Anthropic.SDK/Messaging/ContentType.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Text; @@ -35,10 +35,20 @@ public enum ContentType web_search_tool_result_error, + web_fetch_tool_result, + + web_fetch_result, + + web_fetch_tool_error, + mcp_tool_use, mcp_tool_result, + code_execution_tool_result, + + code_execution_tool_result_error, + bash_code_execution_tool_result, bash_code_execution_result, diff --git a/Anthropic.SDK/Messaging/Message.cs b/Anthropic.SDK/Messaging/Message.cs index 8a1584b..aa1001e 100644 --- a/Anthropic.SDK/Messaging/Message.cs +++ b/Anthropic.SDK/Messaging/Message.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -66,248 +66,252 @@ public Message(Function toolCall, string data, string mediaType, bool isError = Role = RoleType.User; } + /// + /// Reconstructs a single Message from a list of streaming MessageResponse events. + /// Uses a single ordered pass to preserve the natural stream order of content blocks, + /// which is required for multi-turn conversations where the API validates that every + /// server_tool_use has its corresponding result block in the correct position. + /// public Message(List asyncResponses) { Content = []; - var arguments = string.Empty; - var text = string.Empty; - var thinking = string.Empty; - var signature = string.Empty; - var data = string.Empty; - var name = string.Empty; - bool captureTool = false; - var id = string.Empty; - foreach (var result in asyncResponses) - { - if (result.ContentBlock?.Type == "redacted_thinking") - { - data = result.ContentBlock.Data; - } - } - if (!string.IsNullOrWhiteSpace(data)) - { - Content.Add(new RedactedThinkingContent() - { - Data = data - }); - } + string currentBlockType = null; - foreach (var result in asyncResponses) - { - if (!string.IsNullOrWhiteSpace(result.Delta?.Thinking)) - { - thinking += result.Delta.Thinking; - } - if (!string.IsNullOrWhiteSpace(result.Delta?.Signature)) - { - signature += result.Delta.Signature; - } - } - if (!string.IsNullOrWhiteSpace(thinking)) - { - Content.Add(new ThinkingContent() - { - Thinking = thinking, - Signature = signature - }); - } + var textAccum = string.Empty; + CitationResult citationAccum = null; + var thinkingAccum = string.Empty; + var signatureAccum = string.Empty; + var partialJsonAccum = string.Empty; + string blockName = null; + string blockId = null; + string blockServerName = null; - var innerText = string.Empty; - CitationResult citation = null; - foreach (var result in asyncResponses) - { - if ((result.Type != "content_block_stop")) - { - if (result.Delta?.Type == "text_delta") - { - innerText += result.Delta?.Text ?? string.Empty; - } - - citation ??= result.Delta?.Citation; - } - else if (result.Type == "content_block_stop") - { - if (!string.IsNullOrEmpty(innerText)) - { - Content.Add(new TextContent() - { - Text = innerText, - Citations = citation != null ? [citation] : null - }); - } - - innerText = string.Empty; - citation = null; - } - - } - - //if (!string.IsNullOrEmpty(innerText)) - //{ - // Content.Add(new TextContent() - // { - // Text = innerText - // }); - //} - //find server_tool_use - var serverToolUseFound = false; - ServerToolUseContent serverToolUseContent = null; - var serverPartialJson = string.Empty; - foreach (var result in asyncResponses) - { - if (result.ContentBlock != null && result.ContentBlock.Type == "server_tool_use") - { - serverToolUseFound = true; - serverToolUseContent = new ServerToolUseContent() - { - Name = result.ContentBlock.Name, - Id = result.ContentBlock.Id - }; - } - if (serverToolUseFound && !string.IsNullOrWhiteSpace(result.Delta?.PartialJson)) - { - serverPartialJson += result.Delta.PartialJson; - - } - else if (serverToolUseFound && string.IsNullOrWhiteSpace(result.Delta?.PartialJson) && !string.IsNullOrWhiteSpace(serverPartialJson)) - { - var input = JsonSerializer.Deserialize(serverPartialJson); - serverToolUseContent.Input = input; - serverToolUseFound = false; // reset for next tool use - Content.Add(serverToolUseContent); - } - } + ContentBlock pendingImmediateBlock = null; - var mcpToolUseFound = false; - MCPToolUseContent mcpToolUseContent = null; - var mcpPartialJson = string.Empty; foreach (var result in asyncResponses) { - if (result.ContentBlock != null && result.ContentBlock.Type == "mcp_tool_use") + // --- content_block_start: initialize state for this block --- + if (result.ContentBlock != null && result.Type == "content_block_start") { - mcpToolUseFound = true; - mcpToolUseContent = new MCPToolUseContent() + currentBlockType = result.ContentBlock.Type; + textAccum = string.Empty; + citationAccum = null; + thinkingAccum = string.Empty; + signatureAccum = string.Empty; + partialJsonAccum = string.Empty; + blockName = result.ContentBlock.Name; + blockId = result.ContentBlock.Id; + blockServerName = result.ContentBlock.ServerName; + pendingImmediateBlock = null; + + switch (currentBlockType) { - Name = result.ContentBlock.Name, - Id = result.ContentBlock.Id, - ServerName = result.ContentBlock.ServerName - }; - } - if (mcpToolUseFound && !string.IsNullOrWhiteSpace(result.Delta?.PartialJson)) - { - mcpPartialJson += result.Delta.PartialJson; + case "redacted_thinking": + if (!string.IsNullOrWhiteSpace(result.ContentBlock.Data)) + { + Content.Add(new RedactedThinkingContent { Data = result.ContentBlock.Data }); + } + currentBlockType = null; + break; + + case "code_execution_tool_result": + case "mcp_tool_result": + case "web_search_tool_result": + case "web_fetch_tool_result": + case "bash_code_execution_tool_result": + case "text_editor_code_execution_tool_result": + default: + pendingImmediateBlock = result.ContentBlock; + break; + } + continue; } - else if (mcpToolUseFound && string.IsNullOrWhiteSpace(result.Delta?.PartialJson) && !string.IsNullOrWhiteSpace(mcpPartialJson)) - { - var input = JsonNode.Parse(mcpPartialJson); - mcpToolUseContent.Input = input; - mcpToolUseFound = false; // reset for next tool use - Content.Add(mcpToolUseContent); - } - } - var mcpToolResultFound = false; - MCPToolResultContent mcpToolResultContent = null; - foreach (var result in asyncResponses) - { - if (result.ContentBlock != null && result.ContentBlock.Type == "mcp_tool_result") + // --- content_block_delta: accumulate data --- + if (result.Delta != null && currentBlockType != null) { - mcpToolResultFound = true; - mcpToolResultContent = new MCPToolResultContent() - { - ToolUseId = result.ContentBlock.ToolUseId, - Content = result.ContentBlock.Content, - IsError = result.ContentBlock.IsError - }; - Content.Add(mcpToolResultContent); - } + if (!string.IsNullOrEmpty(result.Delta.Text)) + textAccum += result.Delta.Text; - } + if (!string.IsNullOrEmpty(result.Delta.Thinking)) + thinkingAccum += result.Delta.Thinking; - + if (!string.IsNullOrEmpty(result.Delta.Signature)) + signatureAccum += result.Delta.Signature; - var webToolResultFound = false; - WebSearchToolResultContent webToolUseContent = null; - var webSearchPartialJson = string.Empty; - foreach (var result in asyncResponses) - { - if (result.ContentBlock != null && result.ContentBlock.Type == "web_search_tool_result") - { - webToolResultFound = true; - webToolUseContent = new WebSearchToolResultContent() - { - ToolUseId = result.ContentBlock.ToolUseId, - Content = result.ContentBlock.Content, - IsError = result.ContentBlock.IsError - }; - Content.Add(webToolUseContent); - } - - } + if (!string.IsNullOrEmpty(result.Delta.PartialJson)) + partialJsonAccum += result.Delta.PartialJson; - - BashCodeExecutionToolResultContent bashCodeExecutionToolResultContent = null; - foreach (var result in asyncResponses) - { - if (result.ContentBlock != null && result.ContentBlock.Type == "bash_code_execution_tool_result") - { - - bashCodeExecutionToolResultContent = new BashCodeExecutionToolResultContent() - { - ToolUseId = result.ContentBlock.ToolUseId, - Content = result.ContentBlock.Content.FirstOrDefault() - }; - Content.Add(bashCodeExecutionToolResultContent); + citationAccum ??= result.Delta.Citation; } - } - TextEditorCodeExecutionToolResultContent textEditorCodeExecutionToolResultContent = null; - foreach (var result in asyncResponses) - { - if (result.ContentBlock != null && result.ContentBlock.Type == "text_editor_code_execution_tool_result") + // --- content_block_stop: finalize and emit --- + if (result.Type == "content_block_stop" && currentBlockType != null) { - - textEditorCodeExecutionToolResultContent = new TextEditorCodeExecutionToolResultContent() + switch (currentBlockType) { - ToolUseId = result.ContentBlock.ToolUseId, - Content = result.ContentBlock.Content.FirstOrDefault() - }; - Content.Add(textEditorCodeExecutionToolResultContent); - } - } - - - foreach (var result in asyncResponses) - { - if (result.ContentBlock != null && result.ContentBlock.Type == "tool_use") - { - arguments = string.Empty; - captureTool = true; - name = result.ContentBlock.Name; - id = result.ContentBlock.Id; - } + case "text": + if (!string.IsNullOrEmpty(textAccum)) + { + Content.Add(new TextContent + { + Text = textAccum, + Citations = citationAccum != null ? [citationAccum] : null + }); + } + break; + + case "thinking": + if (!string.IsNullOrWhiteSpace(thinkingAccum)) + { + Content.Add(new ThinkingContent + { + Thinking = thinkingAccum, + Signature = signatureAccum + }); + } + break; + + case "server_tool_use": + var serverContent = new ServerToolUseContent + { + Name = blockName, + Id = blockId, + Input = !string.IsNullOrWhiteSpace(partialJsonAccum) + ? JsonSerializer.Deserialize(partialJsonAccum) + : new ServerToolInput() + }; + Content.Add(serverContent); + break; + + case "tool_use": + if (!string.IsNullOrWhiteSpace(partialJsonAccum)) + { + Content.Add(new ToolUseContent + { + Name = blockName, + Id = blockId, + Input = JsonNode.Parse(partialJsonAccum) + }); + } + break; + + case "mcp_tool_use": + var mcpContent = new MCPToolUseContent + { + Name = blockName, + Id = blockId, + ServerName = blockServerName + }; + if (!string.IsNullOrWhiteSpace(partialJsonAccum)) + { + mcpContent.Input = JsonNode.Parse(partialJsonAccum); + } + Content.Add(mcpContent); + break; + + case "mcp_tool_result": + if (pendingImmediateBlock != null) + { + Content.Add(new MCPToolResultContent + { + ToolUseId = pendingImmediateBlock.ToolUseId, + Content = pendingImmediateBlock.Content, + IsError = pendingImmediateBlock.IsError + }); + } + break; + + case "web_search_tool_result": + if (pendingImmediateBlock != null) + { + Content.Add(new WebSearchToolResultContent + { + ToolUseId = pendingImmediateBlock.ToolUseId, + Content = pendingImmediateBlock.Content, + IsError = pendingImmediateBlock.IsError + }); + } + break; + + case "web_fetch_tool_result": + if (pendingImmediateBlock != null) + { + Content.Add(new WebFetchToolResultContent + { + ToolUseId = pendingImmediateBlock.ToolUseId, + Content = pendingImmediateBlock.Content?.FirstOrDefault() + }); + } + break; + + case "code_execution_tool_result": + if (pendingImmediateBlock != null) + { + Content.Add(new CodeExecutionToolResultContent + { + ToolUseId = pendingImmediateBlock.ToolUseId, + Content = pendingImmediateBlock.Content?.FirstOrDefault() + }); + } + break; + + case "bash_code_execution_tool_result": + if (pendingImmediateBlock != null) + { + Content.Add(new BashCodeExecutionToolResultContent + { + ToolUseId = pendingImmediateBlock.ToolUseId, + Content = pendingImmediateBlock.Content?.FirstOrDefault() + }); + } + break; + + case "text_editor_code_execution_tool_result": + if (pendingImmediateBlock != null) + { + Content.Add(new TextEditorCodeExecutionToolResultContent + { + ToolUseId = pendingImmediateBlock.ToolUseId, + Content = pendingImmediateBlock.Content?.FirstOrDefault() + }); + } + break; + + default: + if (pendingImmediateBlock != null) + { + Content.Add(new UnknownContent + { + OriginalType = currentBlockType, + RawJson = JsonSerializer.Serialize(pendingImmediateBlock) + }); + } + break; + } - if (!string.IsNullOrWhiteSpace(result.Delta?.PartialJson)) - { - arguments += result.Delta.PartialJson; + currentBlockType = null; + pendingImmediateBlock = null; + continue; } - if (captureTool && result.Delta?.StopReason == "tool_use") + // Fallback: tool_use blocks may also finalize via stop_reason in message_delta + if (currentBlockType == "tool_use" && result.Delta?.StopReason == "tool_use" + && !string.IsNullOrWhiteSpace(partialJsonAccum)) { - Content.Add(new ToolUseContent() + Content.Add(new ToolUseContent { - Name = name, - Id = id, - Input = JsonNode.Parse(arguments) + Name = blockName, + Id = blockId, + Input = JsonNode.Parse(partialJsonAccum) }); - captureTool = false; + currentBlockType = null; } } Role = RoleType.Assistant; - } /// diff --git a/Anthropic.SDK/Messaging/MessageResponse.cs b/Anthropic.SDK/Messaging/MessageResponse.cs index ba9c062..224b501 100644 --- a/Anthropic.SDK/Messaging/MessageResponse.cs +++ b/Anthropic.SDK/Messaging/MessageResponse.cs @@ -1,4 +1,4 @@ -using System; +using System; using Anthropic.SDK.Common; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -57,6 +57,8 @@ public class MessageResponse [JsonPropertyName("container")] public ContainerResponse Container { get; set; } + + } public class StreamMessage @@ -182,6 +184,9 @@ public class Usage [JsonPropertyName("service_tier")] [JsonConverter(typeof(ServiceTierConverter))] public ServiceTier ServiceTier { get; set; } + + [JsonPropertyName("inference_geo")] + public string InferenceGeo { get; set; } } public class CacheCreation @@ -197,5 +202,11 @@ public class ServerToolUse { [JsonPropertyName("web_search_requests")] public int? WebSearchRequests { get; set; } + + [JsonPropertyName("code_execution_requests")] + public int? CodeExecutionRequests { get; set; } + + [JsonPropertyName("web_fetch_requests")] + public int? WebFetchRequests { get; set; } } } diff --git a/Anthropic.SDK/Messaging/ModelPricing.cs b/Anthropic.SDK/Messaging/ModelPricing.cs new file mode 100644 index 0000000..959de40 --- /dev/null +++ b/Anthropic.SDK/Messaging/ModelPricing.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Anthropic.SDK.Messaging +{ + /// + /// Represents per-model pricing data for estimating API request costs. + /// All costs are in USD per million tokens unless otherwise noted. + /// + public class ModelPricing + { + /// + /// Cost per million input tokens. + /// + public decimal InputTokenCostPerMillion { get; } + + /// + /// Cost per million output tokens. + /// + public decimal OutputTokenCostPerMillion { get; } + + /// + /// Cost per million cache read tokens (typically 0.1x input cost). + /// + public decimal CacheReadCostPerMillion { get; } + + /// + /// Cost per million 5-minute cache write tokens (typically 1.25x input cost). + /// + public decimal Cache5mWriteCostPerMillion { get; } + + /// + /// Cost per million 1-hour cache write tokens (typically 2x input cost). + /// + public decimal Cache1hWriteCostPerMillion { get; } + + /// + /// Cost per 1,000 web search requests. Default is $10 per 1,000 searches. + /// + public decimal WebSearchCostPer1000 { get; } + + public ModelPricing( + decimal inputTokenCostPerMillion, + decimal outputTokenCostPerMillion, + decimal? cacheReadCostPerMillion = null, + decimal? cache5mWriteCostPerMillion = null, + decimal? cache1hWriteCostPerMillion = null, + decimal? webSearchCostPer1000 = null) + { + InputTokenCostPerMillion = inputTokenCostPerMillion; + OutputTokenCostPerMillion = outputTokenCostPerMillion; + CacheReadCostPerMillion = cacheReadCostPerMillion ?? inputTokenCostPerMillion * 0.1m; + Cache5mWriteCostPerMillion = cache5mWriteCostPerMillion ?? inputTokenCostPerMillion * 1.25m; + Cache1hWriteCostPerMillion = cache1hWriteCostPerMillion ?? inputTokenCostPerMillion * 2m; + WebSearchCostPer1000 = webSearchCostPer1000 ?? 10m; + } + + private static readonly ConcurrentDictionary CustomPricing = new(); + + // Ordered longest-prefix-first so that more specific entries match before shorter ones. + private static readonly List<(string Prefix, ModelPricing Pricing)> BuiltInPricing = new() + { + // Opus 4.6 / 4.5 — $5 input, $25 output + ("claude-opus-4-6", new ModelPricing(5m, 25m)), + ("claude-opus-4-5", new ModelPricing(5m, 25m)), + + // Opus 4.1 — $15 input, $75 output + ("claude-opus-4-1", new ModelPricing(15m, 75m)), + + // Opus 4 — $15 input, $75 output + ("claude-opus-4", new ModelPricing(15m, 75m)), + + // Sonnet 4.6 — $3 input, $15 output + ("claude-sonnet-4-6", new ModelPricing(3m, 15m)), + + // Sonnet 4.5 — $3 input, $15 output + ("claude-sonnet-4-5", new ModelPricing(3m, 15m)), + + // Sonnet 4 — $3 input, $15 output + ("claude-sonnet-4", new ModelPricing(3m, 15m)), + + // Sonnet 3.7 — $3 input, $15 output + ("claude-3-7-sonnet", new ModelPricing(3m, 15m)), + + // Haiku 4.5 — $1 input, $5 output + ("claude-haiku-4-5", new ModelPricing(1m, 5m)), + + // Haiku 3.5 — $0.80 input, $4 output + ("claude-3-5-haiku", new ModelPricing(0.80m, 4m)), + }; + + /// + /// Register or override pricing for a model ID prefix. + /// Custom registrations take priority over built-in pricing. + /// + /// The model ID or prefix to match (e.g. "claude-sonnet-4-6"). + /// The pricing to use for matching models. + public static void Register(string modelIdPrefix, ModelPricing pricing) + { + if (string.IsNullOrWhiteSpace(modelIdPrefix)) + throw new ArgumentException("Model ID prefix cannot be null or empty.", nameof(modelIdPrefix)); + if (pricing == null) + throw new ArgumentNullException(nameof(pricing)); + + CustomPricing[modelIdPrefix] = pricing; + } + + /// + /// Remove a previously registered custom pricing entry. + /// + public static bool Unregister(string modelIdPrefix) + { + return CustomPricing.TryRemove(modelIdPrefix, out _); + } + + /// + /// Clear all custom pricing registrations, reverting to built-in pricing only. + /// + public static void ClearCustomPricing() + { + CustomPricing.Clear(); + } + + /// + /// Look up pricing for a given model ID. Custom registrations are checked first + /// (longest prefix match), then built-in pricing. + /// + /// The matching , or null if no match is found. + public static ModelPricing ForModel(string modelId) + { + if (string.IsNullOrWhiteSpace(modelId)) + return null; + + // Check custom registrations first (longest prefix match) + if (!CustomPricing.IsEmpty) + { + var customMatch = CustomPricing.Keys + .Where(prefix => modelId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(prefix => prefix.Length) + .FirstOrDefault(); + + if (customMatch != null) + return CustomPricing[customMatch]; + } + + // Fall back to built-in pricing (already ordered longest-first) + foreach (var (prefix, pricing) in BuiltInPricing) + { + if (modelId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + return pricing; + } + + return null; + } + } +} diff --git a/Anthropic.SDK/Messaging/ServerTools.cs b/Anthropic.SDK/Messaging/ServerTools.cs index 7968ad8..68d2743 100644 --- a/Anthropic.SDK/Messaging/ServerTools.cs +++ b/Anthropic.SDK/Messaging/ServerTools.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using System.Text.Json.Serialization; @@ -8,8 +8,34 @@ namespace Anthropic.SDK.Messaging { public class ServerTools { + /// + /// Default web search tool version with dynamic filtering support (requires code execution tool). + /// + public const string WebSearchVersionDynamicFiltering = "web_search_20260209"; + + /// + /// Legacy web search tool version without dynamic filtering. + /// + public const string WebSearchVersionLegacy = "web_search_20250305"; + + /// + /// Default web fetch tool version with dynamic filtering support (requires code execution tool). + /// + public const string WebFetchVersionDynamicFiltering = "web_fetch_20260209"; + + /// + /// Legacy web fetch tool version without dynamic filtering. + /// + public const string WebFetchVersionLegacy = "web_fetch_20250910"; + + /// + /// Creates a web search tool configuration. + /// The default version (web_search_20260209) supports dynamic filtering with Claude Opus 4.6 and Sonnet 4.6, + /// which requires the code execution tool to be enabled. Pass for the older version. + /// public static Common.Tool GetWebSearchTool(int maxUses = 5, List allowedDomains = null, - List blockedDomains = null, UserLocation userLocation = null) + List blockedDomains = null, UserLocation userLocation = null, + string toolVersion = null) { var dict = new Dictionary(); dict.Add("max_uses", maxUses); @@ -26,8 +52,46 @@ public static Common.Tool GetWebSearchTool(int maxUses = 5, List allowed { dict.Add("user_location", userLocation); } - - return new Function("web_search", "web_search_20250305", dict); + + return new Function("web_search", toolVersion ?? WebSearchVersionDynamicFiltering, dict); + } + + /// + /// Creates a web fetch tool configuration. + /// The default version (web_fetch_20260209) supports dynamic filtering with Claude Opus 4.6 and Sonnet 4.6, + /// which requires the code execution tool to be enabled. Pass for the older version. + /// + public static Common.Tool GetWebFetchTool(int? maxUses = null, + List allowedDomains = null, + List blockedDomains = null, + bool enableCitations = false, + int? maxContentTokens = null, + string toolVersion = null) + { + var dict = new Dictionary(); + + if (maxUses.HasValue) + { + dict.Add("max_uses", maxUses.Value); + } + if (allowedDomains != null && allowedDomains.Count > 0) + { + dict.Add("allowed_domains", allowedDomains); + } + if (blockedDomains != null && blockedDomains.Count > 0) + { + dict.Add("blocked_domains", blockedDomains); + } + if (enableCitations) + { + dict.Add("citations", new Dictionary { { "enabled", true } }); + } + if (maxContentTokens.HasValue) + { + dict.Add("max_content_tokens", maxContentTokens.Value); + } + + return new Function("web_fetch", toolVersion ?? WebFetchVersionDynamicFiltering, dict); } public static Common.Tool GetCodeExecutionTool() diff --git a/Anthropic.SDK/Messaging/ToolChoice.cs b/Anthropic.SDK/Messaging/ToolChoice.cs index 0285df8..1a83bcd 100644 --- a/Anthropic.SDK/Messaging/ToolChoice.cs +++ b/Anthropic.SDK/Messaging/ToolChoice.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using System.Text.Json.Serialization; @@ -13,5 +13,8 @@ public class ToolChoice public ToolChoiceType Type { get; set; } = ToolChoiceType.Auto; [JsonPropertyName("name")] public string Name { get; set; } + [JsonPropertyName("disable_parallel_tool_use")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? DisableParallelToolUse { get; set; } } } diff --git a/Anthropic.SDK/VertexAIClient.cs b/Anthropic.SDK/VertexAIClient.cs index 7e4e805..1f4f474 100644 --- a/Anthropic.SDK/VertexAIClient.cs +++ b/Anthropic.SDK/VertexAIClient.cs @@ -16,7 +16,7 @@ public class VertexAIClient : IDisposable /// /// The base URL format for the Vertex AI API /// - public string ApiUrlFormat { get; set; } = "https://{0}-aiplatform.googleapis.com/v1/projects/{1}/locations/{0}/publishers/anthropic/models/{2}"; + public string ApiUrlFormat { get; set; } = "https://aiplatform.googleapis.com/v1/projects/{1}/locations/{0}/publishers/anthropic/models/{2}"; /// /// The API authentication information to use for API calls diff --git a/README.md b/README.md index 2166806..c3d6e23 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Note: This package is not affiliated with, endorsed by, or sponsored by Anthropi - [MCP Connector](#mcp-connector) - [MCP Client Integration](#mcp-client-integration) - [Web Search](#web-search) + - [Web Fetch](#web-fetch) + - [Cost Calculation](#cost-calculation) - [List Models](#list-models) - [Batching](#batching) - [Tools](#tools) @@ -90,7 +92,7 @@ See integration tests for a more complete example. ### Non-Streaming Call -Here's an example of a non-streaming call to the Claude AI API to the new Claude 3.5 Sonnet model: +Here's an example of a non-streaming call to the Claude AI API to the Claude Sonnet 4.6 model: ```csharp var client = new AnthropicClient(); @@ -105,7 +107,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 1024, - Model = AnthropicModels.Claude35Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, }; @@ -131,7 +133,7 @@ Console.WriteLine(finalResult.Message.ToString()); ### Streaming Call -The following is an example of a streaming call to the Claude AI API Model 3 Opus that provides an image for analysis: +The following is an example of a streaming call to the Claude AI API Claude Opus 4.6 that provides an image for analysis: ```csharp string resourceName = "Anthropic.SDK.Tests.Red_Apple.jpg"; @@ -177,7 +179,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 512, - Model = AnthropicModels.Claude3Opus, + Model = AnthropicModels.Claude46Opus, Stream = true, Temperature = 1.0m, }; @@ -206,14 +208,14 @@ The `AnthropicClient` has support for the [message token count endpoint](https:/ var parameters = new MessageCountTokenParameters { Messages = messages, - Model = AnthropicModels.Claude35Haiku + Model = AnthropicModels.Claude45Haiku }; var response = await client.Messages.CountMessageTokensAsync(parameters); Assert.IsTrue(res.InputTokens > 0); ``` ### Extended Thinking -The `AnthropicClient` has support for the [Sonnet 3.7 with extended thinking support](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking). Below is an example of how to use it. Streaming is supported similarly. +The `AnthropicClient` has support for [extended thinking](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking) in compatible models like Claude Opus 4.6 and Sonnet 4.6. Below is an example of how to use it. Streaming is supported similarly. ```csharp var client = new AnthropicClient(); @@ -223,7 +225,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 4096, - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, Thinking = new ThinkingParameters() @@ -252,7 +254,7 @@ IChatClient client = new AnthropicClient().Messages ChatOptions options = new() { - ModelId = AnthropicModels.Claude3Haiku, + ModelId = AnthropicModels.Claude45Haiku, MaxOutputTokens = 512, Tools = [AIFunctionFactory.Create((string personName) => personName switch { "Alice" => "25", @@ -271,7 +273,7 @@ IChatClient client = new AnthropicClient().Messages; ChatOptions options = new() { - ModelId = AnthropicModels.Claude_v2_1, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 512, Temperature = 1.0f, }; @@ -285,7 +287,7 @@ IChatClient client = new AnthropicClient().Messages; ChatOptions options = new() { - ModelId = AnthropicModels.Claude_v2_1, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 512, Temperature = 1.0f, }; @@ -322,7 +324,7 @@ var res = await client.GetResponseAsync( ]) ], new() { - ModelId = AnthropicModels.Claude3Opus, + ModelId = AnthropicModels.Claude46Opus, MaxOutputTokens = 512, Temperature = 0f, }); @@ -334,7 +336,7 @@ 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: +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 Opus 4.6 and Sonnet 4.6: ```csharp using Anthropic.SDK.Extensions; @@ -349,7 +351,7 @@ List messages = new() // Using the extension method for thinking parameters ChatOptions options = new() { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 4096, Temperature = 1.0f, }.WithThinking(4000); // Enable thinking with 4,000 budget tokens @@ -386,7 +388,7 @@ You can also set thinking parameters using a `ThinkingParameters` object: var thinkingParams = new ThinkingParameters { BudgetTokens = 3000 }; ChatOptions options = new() { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 4096, Temperature = 1.0f, }.WithThinking(thinkingParams); @@ -400,7 +402,7 @@ For enhanced thinking capabilities that allow thinking tokens to exceed max_toke // Enable interleaved thinking with budget tokens exceeding max_tokens ChatOptions options = new() { - ModelId = AnthropicModels.Claude37Sonnet, + ModelId = AnthropicModels.Claude46Sonnet, MaxOutputTokens = 4096, Temperature = 1.0f, }.WithInterleavedThinking(8000); // 8,000 thinking tokens with 4,096 max output tokens @@ -414,6 +416,53 @@ var response = await client.GetResponseAsync("Complex reasoning task", options); - With interleaved thinking enabled, thinking tokens can exceed the max_tokens limit - Developers are responsible for ensuring compatibility with their chosen platform and model +### Adaptive Thinking + +For Claude Opus 4.6 and Sonnet 4.6, adaptive thinking lets Claude dynamically determine when and how much extended thinking to use. This is the recommended approach for these models: + +```csharp +using Anthropic.SDK.Extensions; + +// Adaptive thinking - Claude decides when and how much to think +ChatOptions options = new() +{ + ModelId = AnthropicModels.Claude46Sonnet, + MaxOutputTokens = 4096, +}.WithAdaptiveThinking(); + +var response = await client.GetResponseAsync("Solve this complex problem...", options); +``` + +You can also guide the effort level with `ThinkingEffort`: + +```csharp +// With a specific effort level (low, medium, high, or max) +ChatOptions options = new() +{ + ModelId = AnthropicModels.Claude46Sonnet, + MaxOutputTokens = 4096, +}.WithAdaptiveThinking(ThinkingEffort.high); +``` + +When using the native API directly: + +```csharp +var parameters = new MessageParameters() +{ + Messages = messages, + MaxTokens = 4096, + Model = AnthropicModels.Claude46Sonnet, + Thinking = new ThinkingParameters() + { + Type = ThinkingType.adaptive + }, + OutputConfig = new OutputConfig() + { + Effort = ThinkingEffort.high + } +}; +``` + ### 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. @@ -445,7 +494,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 1024, - Model = AnthropicModels.Claude35Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, System = systemMessages, @@ -494,7 +543,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 1024, - Model = AnthropicModels.Claude35Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 0m, System = systemMessages, @@ -561,7 +610,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 1024, - Model = AnthropicModels.Claude35Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 0m, PromptCaching = PromptCacheType.FineGrained @@ -597,7 +646,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 1024, - Model = AnthropicModels.Claude35Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 0m }; @@ -616,7 +665,7 @@ The `AnthropicClient` supports the new MCP connector, allowing for server-side c var client = new AnthropicClient(); var parameters = new MessageParameters() { - Model = AnthropicModels.Claude37Sonnet, + Model = AnthropicModels.Claude46Sonnet, MaxTokens = 5000, Temperature = 1, MCPServers = new List() @@ -761,15 +810,16 @@ See the `McpClientTests.cs` in the test project for comprehensive examples. ### Web Search -The `AnthropicClient` supports the Web Search API. -Please note that citations are automatically turned on when Web search is used and you are responsible for displaying links to sources in a production setting per Anthropic documentation. +The `AnthropicClient` supports the Web Search API. The default tool version (`web_search_20260209`) supports **dynamic filtering** with Claude Opus 4.6 and Sonnet 4.6, which allows Claude to write and execute code to filter search results before they reach the context window. Dynamic filtering requires the code execution tool to be enabled alongside web search. + +Please note that citations are automatically turned on when web search is used and you are responsible for displaying links to sources in a production setting per Anthropic documentation. ```csharp var client = new AnthropicClient(); var parameters = new MessageParameters() { - Model = AnthropicModels.Claude37Sonnet, - MaxTokens = 3000, + Model = AnthropicModels.Claude46Sonnet, + MaxTokens = 4096, Temperature = 1, Tools = new List() { @@ -779,7 +829,8 @@ var parameters = new MessageParameters() Region = "California", Country = "US", Timezone = "America/Los_Angeles" - }) + }), + ServerTools.GetCodeExecutionTool() // Required for dynamic filtering }, ToolChoice = new ToolChoice() { @@ -808,8 +859,136 @@ Console.WriteLine("----------------------------------------------"); Console.WriteLine("Final Result:"); Console.WriteLine(res.Content.OfType().Last().Text); ``` + +To use the older web search version without dynamic filtering: + +```csharp +ServerTools.GetWebSearchTool(toolVersion: ServerTools.WebSearchVersionLegacy) +``` + See integration tests for more examples like streaming. +### Web Fetch + +The `AnthropicClient` supports the Web Fetch API, which allows Claude to retrieve full content from specified web pages and PDF documents. Like web search, the default version (`web_fetch_20260209`) supports dynamic filtering with Claude Opus 4.6 and Sonnet 4.6. Use `ServerTools.WebFetchVersionLegacy` for the version without dynamic filtering. + +```csharp +var client = new AnthropicClient(); +var parameters = new MessageParameters() +{ + Model = AnthropicModels.Claude46Sonnet, + MaxTokens = 4096, + Temperature = 1, + Tools = new List() + { + ServerTools.GetWebFetchTool(maxUses: 5, enableCitations: true, + toolVersion: ServerTools.WebFetchVersionLegacy) + }, + ToolChoice = new ToolChoice() + { + Type = ToolChoiceType.Auto + }, +}; +var messages = new List +{ + new Message + { + Role = RoleType.User, + Content = new List + { + new TextContent { Text = "Fetch the content at https://example.com and tell me what it says." } + } + } +}; +parameters.Messages = messages; +var res = await client.Messages.GetClaudeMessageAsync(parameters); +Console.WriteLine(res.Content.OfType().Last().Text); +``` + +**Combined Web Search + Web Fetch:** + +```csharp +var tools = new List() +{ + ServerTools.GetWebSearchTool(maxUses: 3), + ServerTools.GetWebFetchTool(maxUses: 5, enableCitations: true) +}; +``` + +**IChatClient Usage:** + +Since `Microsoft.Extensions.AI` does not have a built-in `HostedWebFetchTool`, the SDK provides the `WithWebFetch()` extension method on `ChatOptions`: + +```csharp +using Anthropic.SDK.Extensions; + +IChatClient client = new AnthropicClient().Messages + .AsBuilder() + .UseFunctionInvocation() + .Build(); + +ChatOptions options = new ChatOptions() +{ + ModelId = AnthropicModels.Claude46Sonnet, + MaxOutputTokens = 4096, +}.WithWebFetch(maxUses: 5, enableCitations: true); + +var res = await client.GetResponseAsync("Fetch https://example.com and summarize it.", options); +Console.WriteLine(res.Text); +``` + +See integration tests for more examples including streaming. + +### Cost Calculation + +The SDK includes a client-side cost estimation utility that calculates the estimated USD cost of a request from its token usage data and built-in model pricing. Pricing is sourced from the [Anthropic pricing page](https://docs.anthropic.com/en/docs/about-claude/pricing) and batch-tier discounts are applied automatically. + +```csharp +using Anthropic.SDK.Extensions; + +var client = new AnthropicClient(); +var parameters = new MessageParameters() +{ + Messages = new List { new Message(RoleType.User, "Hello!") }, + MaxTokens = 1024, + Model = AnthropicModels.Claude46Sonnet, +}; +var response = await client.Messages.GetClaudeMessageAsync(parameters); + +// Calculate estimated cost directly from the response +var cost = response.CalculateCost(); +Console.WriteLine($"Total cost: ${cost.TotalCostUsd:F6}"); +Console.WriteLine($" Input tokens: ${cost.InputTokenCost:F6}"); +Console.WriteLine($" Output tokens: ${cost.OutputTokenCost:F6}"); +Console.WriteLine($" Cache read: ${cost.CacheReadCost:F6}"); +Console.WriteLine($" Cache creation: ${cost.CacheCreationCost:F6}"); +Console.WriteLine($" Web search: ${cost.WebSearchCost:F6}"); + +// Or calculate from a Usage object directly with a model ID +var breakdown = response.Usage.CalculateCost(AnthropicModels.Claude46Sonnet); +``` + +**Custom / Enterprise Pricing** + +If you have negotiated pricing or need to add support for a model not yet in the built-in table, you can register custom pricing at runtime: + +```csharp +using Anthropic.SDK.Messaging; + +// Register custom pricing for a model prefix +ModelPricing.Register("claude-sonnet-4-6", new ModelPricing( + inputTokenCostPerMillion: 2.5m, + outputTokenCostPerMillion: 12.5m)); + +// Or pass override pricing directly +var customPricing = new ModelPricing( + inputTokenCostPerMillion: 4m, + outputTokenCostPerMillion: 20m); +var cost = response.CalculateCost(overridePricing: customPricing); + +// Clear all custom registrations to revert to built-in pricing +ModelPricing.ClearCustomPricing(); +``` ### List Models @@ -843,7 +1022,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 512, - Model = AnthropicModels.Claude35Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, }; @@ -910,7 +1089,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 2048, - Model = AnthropicModels.Claude3Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, Tools = tools.ToList() @@ -939,7 +1118,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 512, - Model = AnthropicModels.Claude35Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = true, Temperature = 1.0m, Tools = tools.ToList() @@ -1001,7 +1180,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 2048, - Model = AnthropicModels.Claude3Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, Tools = tools @@ -1046,7 +1225,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 2048, - Model = AnthropicModels.Claude3Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, Tools = tools @@ -1126,7 +1305,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 2048, - Model = AnthropicModels.Claude3Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, Tools = tools @@ -1239,7 +1418,7 @@ var parameters = new MessageParameters() { Messages = messages, MaxTokens = 1024, - Model = AnthropicModels.Claude3Sonnet, + Model = AnthropicModels.Claude46Sonnet, Stream = false, Temperature = 1.0m, Tools = tools, @@ -1542,7 +1721,7 @@ var parameters = new MessageParameters { Messages = messages, MaxTokens = 1024, - Model = AnthropicModels.Claude35Sonnet + Model = AnthropicModels.Claude46Sonnet }; var response = await client.Messages.GetClaudeMessageAsync(parameters); @@ -1705,7 +1884,7 @@ var parameters = new MessageParameters { Messages = messages, MaxTokens = 1024, - Model = AnthropicModels.Claude35Sonnet + Model = AnthropicModels.Claude46Sonnet }; var response = await client.Messages.GetClaudeMessageAsync(parameters); @@ -1851,7 +2030,7 @@ var parameters = new MessageParameters Messages = messages, MaxTokens = 1000, Temperature = 0.7m, - Model = Constants.VertexAIModels.Claude37Sonnet + Model = Constants.VertexAIModels.Claude46Sonnet }; // Get a response from Claude via Vertex AI @@ -1867,16 +2046,15 @@ Console.WriteLine($"Response: {response.Content[0]}"); Vertex AI provides access to the following Claude models: -- `VertexAIModels.Claude3Opus`: Powerful model for complex tasks -- `VertexAIModels.Claude3Sonnet`: Balanced Claude model for a wide range of tasks -- `VertexAIModels.Claude3Haiku`: Fastest and most compact model for near-instant responsiveness -- `VertexAIModels.Claude35Sonnet`: High level of intelligence and capability -- `VertexAIModels.Claude35Haiku`: Intelligence at blazing speeds -- `VertexAIModels.Claude37Sonnet`: Highest level of intelligence and capability with toggleable extended thinking -- `VertexAIModels.Claude4Sonnet`: Sonnet model with toggleable extended thinking -- `VertexAIModels.Claude45Sonnet`: Newest Sonnet model with toggleable extended thinking -- `VertexAIModels.Claude4Opus`: Previous Opus Model and powerful thinking model -- `VertexAIModels.Claude41Opus`: Newest Opus Model and most powerful thinking model +- `VertexAIModels.Claude46Opus`: Latest Opus model with adaptive thinking +- `VertexAIModels.Claude46Sonnet`: Latest Sonnet model with adaptive thinking +- `VertexAIModels.Claude45Opus`: Opus 4.5 model +- `VertexAIModels.Claude45Sonnet`: Sonnet 4.5 model with extended thinking +- `VertexAIModels.Claude45Haiku`: Fast and cost-effective model +- `VertexAIModels.Claude41Opus`: Opus 4.1 with extended thinking +- `VertexAIModels.Claude4Opus`: Opus 4 with extended thinking +- `VertexAIModels.Claude4Sonnet`: Sonnet 4 with extended thinking +- `VertexAIModels.Claude3Haiku`: Compact model for near-instant responsiveness ### Streaming Support