< Summary

Information
Class: OpenAiIntegration.ModelPricingData
Assembly: OpenAiIntegration
File(s): /home/runner/work/KicktippAi/KicktippAi/src/OpenAiIntegration/CostCalculationService.cs
Line coverage
100%
Covered lines: 23
Uncovered lines: 0
Coverable lines: 23
Total lines: 165
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/OpenAiIntegration/CostCalculationService.cs

#LineLine coverage
 1using Microsoft.Extensions.Logging;
 2using OpenAI.Chat;
 3
 4namespace OpenAiIntegration;
 5
 6/// <summary>
 7/// Service for calculating and logging OpenAI API costs
 8/// </summary>
 9public class CostCalculationService : ICostCalculationService
 10{
 11    private const decimal FlexPriceMultiplier = 0.5m;
 12
 13    private readonly ILogger<CostCalculationService> _logger;
 14
 15    public CostCalculationService(ILogger<CostCalculationService> logger)
 16    {
 17        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 18    }
 19
 20    public void LogCostBreakdown(string model, ChatTokenUsage usage)
 21    {
 22        LogCostBreakdown(model, usage, serviceTier: null);
 23    }
 24
 25    public void LogCostBreakdown(string model, ChatTokenUsage usage, string? serviceTier)
 26    {
 27        if (ModelPricingData.Pricing.TryGetValue(model, out var pricing))
 28        {
 29            pricing = ApplyServiceTier(pricing, serviceTier);
 30
 31            // Get exact token counts from usage details
 32            var cachedInputTokens = usage.InputTokenDetails?.CachedTokenCount ?? 0;
 33            var uncachedInputTokens = usage.InputTokenCount - cachedInputTokens;
 34            var reasoningOutputTokens = usage.OutputTokenDetails?.ReasoningTokenCount ?? 0;
 35            var textOutputTokens = usage.OutputTokenCount - reasoningOutputTokens;
 36
 37            // Calculate costs for each component
 38            var uncachedInputCost = (uncachedInputTokens / 1_000_000m) * pricing.InputPrice;
 39            var cachedInputCost = pricing.CachedInputPrice.HasValue
 40                ? (cachedInputTokens / 1_000_000m) * pricing.CachedInputPrice.Value
 41                : 0m;
 42            var outputCost = (usage.OutputTokenCount / 1_000_000m) * pricing.OutputPrice;
 43            var totalCost = uncachedInputCost + cachedInputCost + outputCost;
 44
 45            // Log the cost breakdown
 46            _logger.LogInformation("Uncached Input Tokens: {UncachedInputTokens:N0} × ${InputPrice:F2}/1M = ${UncachedIn
 47                uncachedInputTokens, pricing.InputPrice, uncachedInputCost);
 48
 49            if (pricing.CachedInputPrice.HasValue)
 50            {
 51                _logger.LogInformation("Cached Input Tokens: {CachedInputTokens:N0} × ${CachedInputPrice:F3}/1M = ${Cach
 52                    cachedInputTokens, pricing.CachedInputPrice.Value, cachedInputCost);
 53            }
 54
 55            _logger.LogInformation("Reasoning Output Tokens: {ReasoningOutputTokens:N0}",
 56                reasoningOutputTokens);
 57
 58            _logger.LogInformation("Text Output Tokens: {TextOutputTokens:N0}",
 59                textOutputTokens);
 60
 61            _logger.LogInformation("Total Output Tokens: {TotalOutputTokens:N0} × ${OutputPrice:F2}/1M = ${OutputCost:F6
 62                usage.OutputTokenCount, pricing.OutputPrice, outputCost);
 63
 64            _logger.LogInformation("Total Cost: ${TotalCost:F6}", totalCost);
 65        }
 66        else
 67        {
 68            _logger.LogWarning("Cost calculation not available: Pricing information not found for model '{Model}'", mode
 69        }
 70    }
 71
 72    public decimal? CalculateCost(string model, ChatTokenUsage usage)
 73    {
 74        return CalculateCost(model, usage, serviceTier: null);
 75    }
 76
 77    public decimal? CalculateCost(string model, ChatTokenUsage usage, string? serviceTier)
 78    {
 79        return CalculateCostBreakdown(model, usage, serviceTier)?.Total;
 80    }
 81
 82    public CostBreakdown? CalculateCostBreakdown(string model, ChatTokenUsage usage)
 83    {
 84        return CalculateCostBreakdown(model, usage, serviceTier: null);
 85    }
 86
 87    public CostBreakdown? CalculateCostBreakdown(string model, ChatTokenUsage usage, string? serviceTier)
 88    {
 89        if (ModelPricingData.Pricing.TryGetValue(model, out var pricing))
 90        {
 91            pricing = ApplyServiceTier(pricing, serviceTier);
 92
 93            // Get exact token counts from usage details
 94            var cachedInputTokens = usage.InputTokenDetails?.CachedTokenCount ?? 0;
 95            var uncachedInputTokens = usage.InputTokenCount - cachedInputTokens;
 96            var outputTokens = usage.OutputTokenCount;
 97
 98            // Calculate costs for each component
 99            var uncachedInputCost = (uncachedInputTokens / 1_000_000m) * pricing.InputPrice;
 100            var cachedInputCost = pricing.CachedInputPrice.HasValue
 101                ? (cachedInputTokens / 1_000_000m) * pricing.CachedInputPrice.Value
 102                : 0m;
 103            var outputCost = (outputTokens / 1_000_000m) * pricing.OutputPrice;
 104
 105            return new CostBreakdown(uncachedInputCost, cachedInputCost, outputCost, uncachedInputCost + cachedInputCost
 106        }
 107
 108        return null;
 109    }
 110
 111    private static ModelPricing ApplyServiceTier(ModelPricing pricing, string? serviceTier)
 112    {
 113        if (!string.Equals(serviceTier?.Trim(), "flex", StringComparison.OrdinalIgnoreCase))
 114        {
 115            return pricing;
 116        }
 117
 118        return new ModelPricing(
 119            pricing.InputPrice * FlexPriceMultiplier,
 120            pricing.OutputPrice * FlexPriceMultiplier,
 121            pricing.CachedInputPrice * FlexPriceMultiplier);
 122    }
 123}
 124
 125/// <summary>
 126/// Static short-context standard pricing data for OpenAI models.
 127/// </summary>
 128/// <remarks>
 129/// Source: <see href="https://developers.openai.com/api/docs/pricing.md">OpenAI API pricing</see>.
 130/// Flex processing applies the same short-context rates as Batch pricing for supported models.
 131/// </remarks>
 132internal static class ModelPricingData
 133{
 1134    public static readonly Dictionary<string, ModelPricing> Pricing = new()
 1135    {
 1136        ["gpt-4.1"] = new(2.00m, 8.00m, 0.50m),
 1137        ["gpt-4.1-mini"] = new(0.40m, 1.60m, 0.10m),
 1138        ["gpt-4.1-nano"] = new(0.10m, 0.40m, 0.025m),
 1139        ["gpt-4.5-preview"] = new(75.00m, 150.00m, 37.50m),
 1140        ["gpt-4o"] = new(2.50m, 10.00m, 1.25m),
 1141        ["gpt-4o-mini"] = new(0.15m, 0.60m, 0.075m),
 1142        ["gpt-5.5"] = new(5.00m, 30.00m, 0.50m),
 1143        ["gpt-5.4"] = new(2.50m, 15.00m, 0.25m),
 1144        ["gpt-5.4-mini"] = new(0.75m, 4.50m, 0.075m),
 1145        ["gpt-5.4-nano"] = new(0.20m, 1.25m, 0.02m),
 1146        ["gpt-5"] = new(1.25m, 10.00m, 0.125m),
 1147        ["gpt-5-mini"] = new(0.25m, 2.00m, 0.025m),
 1148        ["gpt-5-nano"] = new(0.05m, 0.40m, 0.005m),
 1149        ["o1"] = new(15.00m, 60.00m, 7.50m),
 1150        ["o1-pro"] = new(150.00m, 600.00m),
 1151        ["o3"] = new(2.00m, 8.00m, 0.50m),
 1152        ["o3-pro"] = new(20.00m, 80.00m),
 1153        ["o4-mini"] = new(1.10m, 4.40m, 0.275m),
 1154        ["o3-mini"] = new(1.10m, 4.40m, 0.55m),
 1155        ["o1-mini"] = new(1.10m, 4.40m, 0.55m),
 1156    };
 157}
 158
 159/// <summary>
 160/// Pricing information for an OpenAI model
 161/// </summary>
 162/// <param name="InputPrice">Price per 1M input tokens</param>
 163/// <param name="OutputPrice">Price per 1M output tokens</param>
 164/// <param name="CachedInputPrice">Price per 1M cached input tokens (if supported)</param>
 165internal record ModelPricing(decimal InputPrice, decimal OutputPrice, decimal? CachedInputPrice = null);

Methods/Properties

.cctor()