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/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/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/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/README.md b/README.md index 2166806..32cab5a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ 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) + - [Cost Calculation](#cost-calculation) - [List Models](#list-models) - [Batching](#batching) - [Tools](#tools) @@ -810,6 +811,56 @@ Console.WriteLine(res.Content.OfType().Last().Text); ``` See integration tests for more examples like 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