< Summary

Information
Class: OpenAiIntegration.TokenUsageTracker
Assembly: OpenAiIntegration
File(s): /home/runner/work/KicktippAi/KicktippAi/src/OpenAiIntegration/TokenUsageTracker.cs
Line coverage
100%
Covered lines: 98
Uncovered lines: 0
Coverable lines: 98
Total lines: 207
Line coverage: 100%
Branch coverage
100%
Covered branches: 22
Total branches: 22
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%44100%
AddUsage(...)100%11100%
AddUsage(...)100%66100%
GetCompactSummary()100%11100%
GetCompactSummaryWithEstimatedCosts(...)100%11100%
CalculateTotalEstimatedCost(...)100%44100%
GetLastUsageCompactSummary()100%11100%
GetLastUsageCompactSummaryWithEstimatedCosts(...)100%22100%
GetLastUsageJson()100%66100%
GetTotalCost()100%11100%
GetLastCost()100%11100%
Reset()100%11100%

File(s)

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

#LineLine coverage
 1using System.Globalization;
 2using System.Text.Json;
 3using Microsoft.Extensions.Logging;
 4using OpenAI.Chat;
 5
 6namespace OpenAiIntegration;
 7
 8/// <summary>
 9/// Service for tracking token usage and costs across multiple API calls
 10/// </summary>
 11public class TokenUsageTracker : ITokenUsageTracker
 12{
 13    private readonly ILogger<TokenUsageTracker> _logger;
 14    private readonly ICostCalculationService _costCalculationService;
 115    private readonly object _lock = new();
 16
 17    private int _totalUncachedInputTokens = 0;
 18    private int _totalCachedInputTokens = 0;
 19    private int _totalOutputReasoningTokens = 0;
 20    private int _totalOutputTokens = 0;
 21    private decimal _totalCost = 0m;
 22
 23    // Track last usage for individual match reporting
 24    private int _lastUncachedInputTokens = 0;
 25    private int _lastCachedInputTokens = 0;
 26    private int _lastOutputReasoningTokens = 0;
 27    private int _lastOutputTokens = 0;
 28    private decimal _lastCost = 0m;
 29    private ChatTokenUsage? _lastUsage = null;
 30
 131    public TokenUsageTracker(ILogger<TokenUsageTracker> logger, ICostCalculationService costCalculationService)
 32    {
 133        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 134        _costCalculationService = costCalculationService ?? throw new ArgumentNullException(nameof(costCalculationServic
 135    }
 36
 37    public void AddUsage(string model, ChatTokenUsage usage)
 38    {
 139        AddUsage(model, usage, serviceTier: null);
 140    }
 41
 42    public void AddUsage(string model, ChatTokenUsage usage, string? serviceTier)
 43    {
 144        lock (_lock)
 45        {
 46            // Store last usage for individual reporting
 147            _lastUsage = usage;
 48
 49            // Get token counts
 150            var cachedInputTokens = usage.InputTokenDetails?.CachedTokenCount ?? 0;
 151            var uncachedInputTokens = usage.InputTokenCount - cachedInputTokens;
 152            var outputReasoningTokens = usage.OutputTokenDetails?.ReasoningTokenCount ?? 0;
 153            var regularOutputTokens = usage.OutputTokenCount - outputReasoningTokens;
 54
 55            // Store last usage for individual reporting
 156            _lastUncachedInputTokens = uncachedInputTokens;
 157            _lastCachedInputTokens = cachedInputTokens;
 158            _lastOutputReasoningTokens = outputReasoningTokens;
 159            _lastOutputTokens = regularOutputTokens;
 60
 61            // Add to totals
 162            _totalUncachedInputTokens += uncachedInputTokens;
 163            _totalCachedInputTokens += cachedInputTokens;
 164            _totalOutputReasoningTokens += outputReasoningTokens;
 165            _totalOutputTokens += regularOutputTokens;
 66
 67            // Calculate cost for this usage
 168            var costForThisUsage = string.IsNullOrWhiteSpace(serviceTier)
 169                ? _costCalculationService.CalculateCost(model, usage) ?? 0m
 170                : _costCalculationService.CalculateCost(model, usage, serviceTier) ?? 0m;
 171            _lastCost = costForThisUsage;
 172            _totalCost += costForThisUsage;
 73
 174            _logger.LogDebug("Added usage for model {Model}: {UncachedInput} uncached + {CachedInput} cached + {OutputRe
 175                model, uncachedInputTokens, cachedInputTokens, outputReasoningTokens, regularOutputTokens, costForThisUs
 176        }
 177    }
 78
 79    public string GetCompactSummary()
 80    {
 181        lock (_lock)
 82        {
 183            return $"{_totalUncachedInputTokens.ToString("N0", CultureInfo.InvariantCulture)} / {_totalCachedInputTokens
 84        }
 185    }
 86
 87    public string GetCompactSummaryWithEstimatedCosts(string estimatedCostsModel)
 88    {
 189        lock (_lock)
 90        {
 191            var baseSummary = $"{_totalUncachedInputTokens.ToString("N0", CultureInfo.InvariantCulture)} / {_totalCached
 92
 93            // Calculate estimated costs for the alternative model
 194            decimal totalEstimatedCost = CalculateTotalEstimatedCost(estimatedCostsModel);
 95
 196            return $"{baseSummary} (est {estimatedCostsModel}: ${totalEstimatedCost.ToString("F4", CultureInfo.Invariant
 97        }
 198    }
 99
 100    private decimal CalculateTotalEstimatedCost(string estimatedCostsModel)
 101    {
 102        // Manually calculate estimated cost based on our tracked totals
 1103        if (!ModelPricingData.Pricing.TryGetValue(estimatedCostsModel, out var pricing))
 104        {
 1105            return 0m; // Can't calculate if we don't have pricing info
 106        }
 107
 108        // Calculate costs for each component
 1109        var uncachedInputCost = (_totalUncachedInputTokens / 1_000_000m) * pricing.InputPrice;
 1110        var cachedInputCost = pricing.CachedInputPrice.HasValue
 1111            ? (_totalCachedInputTokens / 1_000_000m) * pricing.CachedInputPrice.Value
 1112            : 0m;
 1113        var totalOutputTokenCount = _totalOutputReasoningTokens + _totalOutputTokens;
 1114        var outputCost = (totalOutputTokenCount / 1_000_000m) * pricing.OutputPrice;
 115
 1116        return uncachedInputCost + cachedInputCost + outputCost;
 117    }
 118
 119    public string GetLastUsageCompactSummary()
 120    {
 1121        lock (_lock)
 122        {
 1123            return $"{_lastUncachedInputTokens.ToString("N0", CultureInfo.InvariantCulture)} / {_lastCachedInputTokens.T
 124        }
 1125    }
 126
 127    public string GetLastUsageCompactSummaryWithEstimatedCosts(string estimatedCostsModel)
 128    {
 1129        lock (_lock)
 130        {
 1131            var baseSummary = $"{_lastUncachedInputTokens.ToString("N0", CultureInfo.InvariantCulture)} / {_lastCachedIn
 132
 133            // Calculate estimated cost for last usage
 1134            if (_lastUsage != null)
 135            {
 1136                var estimatedCost = _costCalculationService.CalculateCost(estimatedCostsModel, _lastUsage) ?? 0m;
 1137                return $"{baseSummary} (est {estimatedCostsModel}: ${estimatedCost.ToString("F4", CultureInfo.InvariantC
 138            }
 139
 1140            return baseSummary;
 141        }
 1142    }
 143
 144    public string? GetLastUsageJson()
 145    {
 1146        lock (_lock)
 147        {
 1148            if (_lastUsage == null)
 1149                return null;
 150
 1151            var usageData = new
 1152            {
 1153                InputTokenCount = _lastUsage.InputTokenCount,
 1154                OutputTokenCount = _lastUsage.OutputTokenCount,
 1155                InputTokenDetails = _lastUsage.InputTokenDetails != null ? new
 1156                {
 1157                    CachedTokenCount = _lastUsage.InputTokenDetails.CachedTokenCount,
 1158                    AudioTokenCount = _lastUsage.InputTokenDetails.AudioTokenCount
 1159                } : null,
 1160                OutputTokenDetails = _lastUsage.OutputTokenDetails != null ? new
 1161                {
 1162                    ReasoningTokenCount = _lastUsage.OutputTokenDetails.ReasoningTokenCount,
 1163                    AudioTokenCount = _lastUsage.OutputTokenDetails.AudioTokenCount
 1164                } : null
 1165            };
 166
 1167            return JsonSerializer.Serialize(usageData, new JsonSerializerOptions { WriteIndented = false });
 168        }
 1169    }
 170
 171    public decimal GetTotalCost()
 172    {
 1173        lock (_lock)
 174        {
 1175            return _totalCost;
 176        }
 1177    }
 178
 179    public decimal GetLastCost()
 180    {
 1181        lock (_lock)
 182        {
 1183            return _lastCost;
 184        }
 1185    }
 186
 187    public void Reset()
 188    {
 1189        lock (_lock)
 190        {
 1191            _totalUncachedInputTokens = 0;
 1192            _totalCachedInputTokens = 0;
 1193            _totalOutputReasoningTokens = 0;
 1194            _totalOutputTokens = 0;
 1195            _totalCost = 0m;
 196
 1197            _lastUncachedInputTokens = 0;
 1198            _lastCachedInputTokens = 0;
 1199            _lastOutputReasoningTokens = 0;
 1200            _lastOutputTokens = 0;
 1201            _lastCost = 0m;
 1202            _lastUsage = null;
 203
 1204            _logger.LogDebug("Token usage tracker reset");
 1205        }
 1206    }
 207}