Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Anthropic.SDK.Tests/Anthropic.SDK.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@

<ItemGroup>
<PackageReference Include="Google.Cloud.AIPlatform.V1" Version="3.65.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="11.0.0-preview.1.26104.118" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.AI" Version="10.3.0" />
<PackageReference Include="ModelContextProtocol" Version="0.8.0-preview.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="11.0.0-preview.1.26104.118" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="11.0.0-preview.1.26104.118" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="11.0.0-preview.1.26104.118" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.71.0" />
<PackageReference Include="Microsoft.SemanticKernel.Abstractions" Version="1.71.0" />
Expand Down
38 changes: 38 additions & 0 deletions Anthropic.SDK.Tests/CostTest.cs
Original file line number Diff line number Diff line change
@@ -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<Message> { 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}");
}
}
}
16 changes: 8 additions & 8 deletions Anthropic.SDK.Tests/MCPTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MCPServer>()
{
new MCPServer()
{
Url = "https://mcp.deepwiki.com/sse",
Name = "DeepWiki",
Url = "https://learn.microsoft.com/api/mcp",
Name = "MSFT",
}
}
};
Expand All @@ -37,7 +37,7 @@ public async Task TestMCP()
Role = RoleType.User,
Content = new List<ContentBase>
{
new TextContent { Text = "Tell me about the repo tghamm/Anthropic.SDK" }
new TextContent { Text = "Tell me about the Latest Microsoft.Extensions.AI Library" }
}
}
};
Expand All @@ -55,16 +55,16 @@ public async Task TestMCPExtendedStreaming()
var client = new AnthropicClient();
var parameters = new MessageParameters()
{
Model = AnthropicModels.Claude37Sonnet,
Model = AnthropicModels.Claude46Sonnet,
MaxTokens = 3000,
Temperature = 1,
Stream = true,
MCPServers = new List<MCPServer>()
{
new MCPServer()
{
Url = "https://mcp.deepwiki.com/sse",
Name = "DeepWiki",
Url = "https://learn.microsoft.com/api/mcp",
Name = "MSFT",
}
}
};
Expand All @@ -75,7 +75,7 @@ public async Task TestMCPExtendedStreaming()
Role = RoleType.User,
Content = new List<ContentBase>
{
new TextContent { Text = "Tell me about the repo tghamm/Anthropic.SDK" }
new TextContent { Text = "Tell me about the latest Microsoft.Extensions.AI Library" }
}
}
};
Expand Down
162 changes: 162 additions & 0 deletions Anthropic.SDK/Extensions/CostCalculationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using System;
using Anthropic.SDK.Messaging;

namespace Anthropic.SDK.Extensions
{
/// <summary>
/// Detailed breakdown of estimated costs for an API request.
/// All values are in USD.
/// </summary>
public class CostBreakdown
{
/// <summary>
/// Cost of base input tokens.
/// </summary>
public decimal InputTokenCost { get; set; }

/// <summary>
/// Cost of output tokens.
/// </summary>
public decimal OutputTokenCost { get; set; }

/// <summary>
/// Cost of cache read tokens.
/// </summary>
public decimal CacheReadCost { get; set; }

/// <summary>
/// Cost of cache creation tokens (combined 5-minute and 1-hour).
/// When detailed cache creation breakdown is unavailable, the legacy
/// <c>cache_creation_input_tokens</c> field is priced at the 5-minute write rate.
/// </summary>
public decimal CacheCreationCost { get; set; }

/// <summary>
/// Cost of web search requests ($0.01 per search).
/// </summary>
public decimal WebSearchCost { get; set; }

/// <summary>
/// Total estimated cost in USD (sum of all components).
/// </summary>
public decimal TotalCostUsd =>
InputTokenCost + OutputTokenCost + CacheReadCost + CacheCreationCost + WebSearchCost;

/// <summary>
/// The <see cref="ModelPricing"/> used for this calculation.
/// </summary>
public ModelPricing Pricing { get; set; }
}

/// <summary>
/// Extension methods for calculating estimated API costs from usage data.
/// </summary>
public static class CostCalculationExtensions
{
private const decimal PerMillionDivisor = 1_000_000m;
private const decimal Per1000Divisor = 1_000m;

/// <summary>
/// Calculate the estimated cost of an API request from its <see cref="Usage"/> data.
/// When the service tier is <see cref="ServiceTier.Batch"/>, a 50% discount is applied
/// to all token costs automatically.
/// </summary>
/// <param name="usage">The usage data from the API response.</param>
/// <param name="modelId">The model ID string used for the request.</param>
/// <param name="overridePricing">
/// Optional pricing to use instead of the built-in/registered pricing.
/// </param>
/// <returns>A <see cref="CostBreakdown"/> with per-category costs.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="usage"/> is null.</exception>
/// <exception cref="InvalidOperationException">
/// Thrown when no pricing can be found for <paramref name="modelId"/>
/// and <paramref name="overridePricing"/> is not provided.
/// </exception>
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,
};
}

/// <summary>
/// Calculate the estimated cost of an API request directly from the <see cref="MessageResponse"/>.
/// Uses the model from the response and its usage data.
/// </summary>
/// <param name="response">The message response from the API.</param>
/// <param name="overridePricing">
/// Optional pricing to use instead of the built-in/registered pricing.
/// </param>
/// <returns>A <see cref="CostBreakdown"/> with per-category costs.</returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="response"/> or its Usage is null.
/// </exception>
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);
}
}
}
13 changes: 12 additions & 1 deletion Anthropic.SDK/Messaging/MessageResponse.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using Anthropic.SDK.Common;
using System.Collections.Generic;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -38,7 +38,7 @@
public Delta Delta { get; set; }

[JsonPropertyName("content_block")]
public ContentBlock? ContentBlock { get; set; }

Check warning on line 41 in Anthropic.SDK/Messaging/MessageResponse.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

[JsonPropertyName("message")]
public StreamMessage StreamStartMessage { get; set; }
Expand All @@ -57,6 +57,8 @@

[JsonPropertyName("container")]
public ContainerResponse Container { get; set; }


}

public class StreamMessage
Expand Down Expand Up @@ -123,7 +125,7 @@
public string Name { get; set; }

[JsonPropertyName("partial_json")]
public string? PartialJson { get; set; }

Check warning on line 128 in Anthropic.SDK/Messaging/MessageResponse.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[JsonPropertyName("citation")]
public CitationResult Citation { get; set; }
}
Expand All @@ -134,21 +136,21 @@
public string Type { get; set; }

[JsonPropertyName("id")]
public string? Id { get; set; }

Check warning on line 139 in Anthropic.SDK/Messaging/MessageResponse.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

[JsonPropertyName("text")]
public string? Text { get; set; }

Check warning on line 142 in Anthropic.SDK/Messaging/MessageResponse.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

[JsonPropertyName("name")]
public string? Name { get; set; }

Check warning on line 145 in Anthropic.SDK/Messaging/MessageResponse.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

[JsonPropertyName("server_name")]
public string? ServerName { get; set; }

Check warning on line 148 in Anthropic.SDK/Messaging/MessageResponse.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

[JsonPropertyName("data")]
public string? Data { get; set; }

Check warning on line 151 in Anthropic.SDK/Messaging/MessageResponse.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[JsonPropertyName("tool_use_id")]
public string? ToolUseId { get; set; }

Check warning on line 153 in Anthropic.SDK/Messaging/MessageResponse.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
[JsonPropertyName("content")]
[JsonConverter(typeof(SingleOrArrayConverter<ContentBase>))]
public List<ContentBase> Content { get; set; }
Expand Down Expand Up @@ -182,6 +184,9 @@
[JsonPropertyName("service_tier")]
[JsonConverter(typeof(ServiceTierConverter))]
public ServiceTier ServiceTier { get; set; }

[JsonPropertyName("inference_geo")]
public string InferenceGeo { get; set; }
}

public class CacheCreation
Expand All @@ -197,5 +202,11 @@
{
[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; }
}
}
Loading
Loading