< Summary

Information
Class: OpenAiIntegration.CostCalculationService
Assembly: OpenAiIntegration
File(s): /home/runner/work/KicktippAi/KicktippAi/src/OpenAiIntegration/CostCalculationService.cs
Line coverage
100%
Covered lines: 39
Uncovered lines: 0
Coverable lines: 39
Total lines: 119
Line coverage: 100%
Branch coverage
94%
Covered branches: 17
Total branches: 18
Branch coverage: 94.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%22100%
LogCostBreakdown(...)100%1010100%
CalculateCost(...)100%66100%

File(s)

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

#LineLine coverage
 1using System.Globalization;
 2using Microsoft.Extensions.Logging;
 3using OpenAI.Chat;
 4
 5namespace OpenAiIntegration;
 6
 7/// <summary>
 8/// Service for calculating and logging OpenAI API costs
 9/// </summary>
 10public class CostCalculationService : ICostCalculationService
 11{
 12    private readonly ILogger<CostCalculationService> _logger;
 13
 114    public CostCalculationService(ILogger<CostCalculationService> logger)
 15    {
 116        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 117    }
 18
 19    public void LogCostBreakdown(string model, ChatTokenUsage usage)
 20    {
 121        if (ModelPricingData.Pricing.TryGetValue(model, out var pricing))
 22        {
 23            // Get exact token counts from usage details
 124            var cachedInputTokens = usage.InputTokenDetails?.CachedTokenCount ?? 0;
 125            var uncachedInputTokens = usage.InputTokenCount - cachedInputTokens;
 126            var reasoningOutputTokens = usage.OutputTokenDetails?.ReasoningTokenCount ?? 0;
 127            var textOutputTokens = usage.OutputTokenCount - reasoningOutputTokens;
 28
 29            // Calculate costs for each component
 130            var uncachedInputCost = (uncachedInputTokens / 1_000_000m) * pricing.InputPrice;
 131            var cachedInputCost = pricing.CachedInputPrice.HasValue
 132                ? (cachedInputTokens / 1_000_000m) * pricing.CachedInputPrice.Value
 133                : 0m;
 134            var outputCost = (usage.OutputTokenCount / 1_000_000m) * pricing.OutputPrice;
 135            var totalCost = uncachedInputCost + cachedInputCost + outputCost;
 36
 37            // Log the cost breakdown
 138            _logger.LogInformation("Uncached Input Tokens: {UncachedInputTokens:N0} × ${InputPrice:F2}/1M = ${UncachedIn
 139                uncachedInputTokens, pricing.InputPrice, uncachedInputCost);
 40
 141            if (pricing.CachedInputPrice.HasValue)
 42            {
 143                _logger.LogInformation("Cached Input Tokens: {CachedInputTokens:N0} × ${CachedInputPrice:F3}/1M = ${Cach
 144                    cachedInputTokens, pricing.CachedInputPrice.Value, cachedInputCost);
 45            }
 46
 147            _logger.LogInformation("Reasoning Output Tokens: {ReasoningOutputTokens:N0}",
 148                reasoningOutputTokens);
 49
 150            _logger.LogInformation("Text Output Tokens: {TextOutputTokens:N0}",
 151                textOutputTokens);
 52
 153            _logger.LogInformation("Total Output Tokens: {TotalOutputTokens:N0} × ${OutputPrice:F2}/1M = ${OutputCost:F6
 154                usage.OutputTokenCount, pricing.OutputPrice, outputCost);
 55
 156            _logger.LogInformation("Total Cost: ${TotalCost:F6}", totalCost);
 57        }
 58        else
 59        {
 160            _logger.LogWarning("Cost calculation not available: Pricing information not found for model '{Model}'", mode
 61        }
 162    }
 63
 64    public decimal? CalculateCost(string model, ChatTokenUsage usage)
 65    {
 166        if (ModelPricingData.Pricing.TryGetValue(model, out var pricing))
 67        {
 68            // Get exact token counts from usage details
 169            var cachedInputTokens = usage.InputTokenDetails?.CachedTokenCount ?? 0;
 170            var uncachedInputTokens = usage.InputTokenCount - cachedInputTokens;
 171            var outputTokens = usage.OutputTokenCount;
 72
 73            // Calculate costs for each component
 174            var uncachedInputCost = (uncachedInputTokens / 1_000_000m) * pricing.InputPrice;
 175            var cachedInputCost = pricing.CachedInputPrice.HasValue
 176                ? (cachedInputTokens / 1_000_000m) * pricing.CachedInputPrice.Value
 177                : 0m;
 178            var outputCost = (outputTokens / 1_000_000m) * pricing.OutputPrice;
 79
 180            return uncachedInputCost + cachedInputCost + outputCost;
 81        }
 82
 183        return null;
 84    }
 85}
 86
 87/// <summary>
 88/// Static pricing data for OpenAI models - matches the structure from PromptSampleTests
 89/// </summary>
 90internal static class ModelPricingData
 91{
 92    public static readonly Dictionary<string, ModelPricing> Pricing = new()
 93    {
 94        ["gpt-4.1"] = new(2.00m, 8.00m, 0.50m),
 95        ["gpt-4.1-mini"] = new(0.40m, 1.60m, 0.10m),
 96        ["gpt-4.1-nano"] = new(0.10m, 0.40m, 0.025m),
 97        ["gpt-4.5-preview"] = new(75.00m, 150.00m, 37.50m),
 98        ["gpt-4o"] = new(2.50m, 10.00m, 1.25m),
 99        ["gpt-4o-mini"] = new(0.15m, 0.60m, 0.075m),
 100        ["gpt-5"] = new(1.25m, 10.00m, 0.125m),
 101        ["gpt-5-mini"] = new(0.25m, 2.00m, 0.025m),
 102        ["gpt-5-nano"] = new(0.05m, 0.40m, 0.005m),
 103        ["o1"] = new(15.00m, 60.00m, 7.50m),
 104        ["o1-pro"] = new(150.00m, 600.00m),
 105        ["o3"] = new(2.00m, 8.00m, 0.50m),
 106        ["o3-pro"] = new(20.00m, 80.00m),
 107        ["o4-mini"] = new(1.10m, 4.40m, 0.275m),
 108        ["o3-mini"] = new(1.10m, 4.40m, 0.55m),
 109        ["o1-mini"] = new(1.10m, 4.40m, 0.55m),
 110    };
 111}
 112
 113/// <summary>
 114/// Pricing information for an OpenAI model
 115/// </summary>
 116/// <param name="InputPrice">Price per 1M input tokens</param>
 117/// <param name="OutputPrice">Price per 1M output tokens</param>
 118/// <param name="CachedInputPrice">Price per 1M cached input tokens (if supported)</param>
 119internal record ModelPricing(decimal InputPrice, decimal OutputPrice, decimal? CachedInputPrice = null);