namespace AnchorCli.OpenRouter;
///
/// Tracks token usage and calculates costs for the session.
///
internal sealed class TokenTracker
{
private ChatSession _session;
public TokenTracker(ChatSession session)
{
_session = session;
}
///
/// Gets or sets the session. Allows setting the session after construction
/// to support dependency injection patterns.
///
public ChatSession Session
{
get => _session;
set => _session = value;
}
public string Provider { get; set; } = "Unknown";
public long SessionInputTokens => _session.SessionInputTokens;
public long SessionOutputTokens => _session.SessionOutputTokens;
public int RequestCount => _session.RequestCount;
/// Maximum context window for the model (tokens). 0 = unknown.
public int ContextLength { get; set; }
/// Input tokens from the most recent API response — approximates current context size.
public int LastInputTokens { get; private set; }
/// USD per input token.
public decimal InputPrice { get; set; }
/// USD per output token.
public decimal OutputPrice { get; set; }
/// Fixed USD per API request.
public decimal RequestPrice { get; set; }
///
/// Record usage from one response (may span multiple LLM rounds).
///
public void AddUsage(int inputTokens, int outputTokens)
{
_session.SessionInputTokens += inputTokens;
_session.SessionOutputTokens += outputTokens;
LastInputTokens = inputTokens;
_session.RequestCount++;
}
public void Reset()
{
_session.SessionInputTokens = 0;
_session.SessionOutputTokens = 0;
_session.RequestCount = 0;
LastInputTokens = 0;
}
private const int MaxContextReserve = 150_000;
///
/// Returns true if the context is getting too large and should be compacted.
/// Triggers at min(75% of model context, 150K tokens).
///
public bool ShouldCompact()
{
if (LastInputTokens <= 0) return false;
int threshold = ContextLength > 0
? Math.Min((int)(ContextLength * 0.75), MaxContextReserve)
: MaxContextReserve;
return LastInputTokens >= threshold;
}
/// Context usage as a percentage (0-100). Returns -1 if context length is unknown.
public double ContextUsagePercent =>
ContextLength > 0 && LastInputTokens > 0
? (double)LastInputTokens / ContextLength * 100.0
: -1;
///
/// Calculate cost for a single response.
///
public decimal CalculateCost(int inputTokens, int outputTokens) =>
inputTokens * InputPrice +
outputTokens * OutputPrice +
RequestPrice;
///
/// Total session cost.
///
public decimal SessionCost =>
SessionInputTokens * InputPrice +
SessionOutputTokens * OutputPrice +
RequestCount * RequestPrice;
public static string FormatTokens(long count) =>
count >= 1_000 ? $"{count / 1_000.0:F1}k" : count.ToString("N0");
public static string FormatCost(decimal cost) =>
cost < 0.01m ? $"${cost:F4}" : $"${cost:F2}";
}