컨텍스트 카드 모델별 사용량 가시화 보강
Some checks failed
Release Gate / gate (push) Has been cancelled

- AX Agent 하단 컨텍스트 카드에 현재 서비스·모델 기준 오늘 사용량을 함께 표시하고 hover에서 현재 모델 usage·compact 이후 usage·오늘 상위 모델 usage를 확인할 수 있게 함

- UsageStatisticsService에 오늘 통계 스냅샷 API를 추가하고 long-safe 토큰 포맷 경로를 넣어 per-model 집계가 커져도 K/M 단위로 안정적으로 표시되게 함

- README.md와 docs/DEVELOPMENT.md에 2026-04-05 00:34 (KST) 기준 이력을 반영함

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ / 경고 0 / 오류 0
This commit is contained in:
2026-04-05 00:22:56 +09:00
parent d40b80ee96
commit f9fd144bd0
4 changed files with 102 additions and 1 deletions

View File

@@ -141,6 +141,16 @@ internal static class UsageStatisticsService
return result;
}
/// <summary>오늘자 통계 스냅샷을 반환합니다.</summary>
public static DailyUsageStats GetTodaySnapshot()
{
EnsureInitialized();
lock (_lock)
{
return CloneToday();
}
}
// ─── 내부 ────────────────────────────────────────────────────────────────
private static void EnsureInitialized()

View File

@@ -16680,9 +16680,22 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
TokenUsageArc.Stroke = progressBrush;
TokenUsageThresholdMarker.Fill = progressBrush;
var percentText = $"{Math.Round(usageRatio * 100):0}%";
var (currentService, currentModel) = _llm.GetCurrentModelInfo();
var todayUsage = Services.UsageStatisticsService.GetTodaySnapshot();
var currentModelUsageKey = BuildUsageModelKey(currentService, currentModel);
var currentModelPromptTokens = GetUsageValue(todayUsage.ModelPromptTokens, currentModelUsageKey);
var currentModelCompletionTokens = GetUsageValue(todayUsage.ModelCompletionTokens, currentModelUsageKey);
var currentModelTotalTokens = currentModelPromptTokens + currentModelCompletionTokens;
var currentModelPostPromptTokens = GetUsageValue(todayUsage.PostCompactionPromptTokens, currentModelUsageKey);
var currentModelPostCompletionTokens = GetUsageValue(todayUsage.PostCompactionCompletionTokens, currentModelUsageKey);
var currentModelPostTotalTokens = currentModelPostPromptTokens + currentModelPostCompletionTokens;
TokenUsagePercentText.Text = percentText;
TokenUsageSummaryText.Text = $"컨텍스트 {percentText}";
TokenUsageHintText.Text = $"{Services.TokenEstimator.Format(currentTokens)} / {Services.TokenEstimator.Format(maxContextTokens)}";
TokenUsageHintText.Text =
$"{Services.TokenEstimator.Format(currentTokens)} / {Services.TokenEstimator.Format(maxContextTokens)}" +
(currentModelTotalTokens > 0
? $" · 오늘 {FormatTokenCount(currentModelTotalTokens)}"
: "");
CompactNowLabel.Text = compactLabel;
var compactHistory = _lastCompactionAt.HasValue && _lastCompactionBeforeTokens.HasValue && _lastCompactionAfterTokens.HasValue
? $"\n최근 압축: {(_lastCompactionWasAutomatic ? "" : "")} · {_lastCompactionAt.Value:HH:mm:ss}\n" +
@@ -16702,6 +16715,14 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
$"compact 이후 사용량: {Services.TokenEstimator.Format(_sessionPostCompactionPromptTokens)} + {Services.TokenEstimator.Format(_sessionPostCompactionCompletionTokens)} = {Services.TokenEstimator.Format(_sessionPostCompactionPromptTokens + _sessionPostCompactionCompletionTokens)} tokens"
: "";
var pendingPostCompaction = _pendingPostCompaction ? "\ncompact 후 첫 응답 대기 중" : "";
var currentModelUsageText = !string.IsNullOrWhiteSpace(currentModelUsageKey)
? $"\n현재 모델: {currentService} · {currentModel}\n" +
$"오늘 모델 사용량: {FormatTokenCount(currentModelPromptTokens)} + {FormatTokenCount(currentModelCompletionTokens)} = {FormatTokenCount(currentModelTotalTokens)} tokens" +
(currentModelPostTotalTokens > 0
? $"\n현재 모델 compact 이후: {FormatTokenCount(currentModelPostPromptTokens)} + {FormatTokenCount(currentModelPostCompletionTokens)} = {FormatTokenCount(currentModelPostTotalTokens)} tokens"
: "")
: "";
var topModelsText = BuildTopModelUsageSummary(todayUsage);
TokenUsageCard.ToolTip =
$"상태: {summary}\n" +
@@ -16709,6 +16730,8 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
$"간단 표기: {Services.TokenEstimator.Format(currentTokens)} / {Services.TokenEstimator.Format(maxContextTokens)}\n" +
$"자동 압축 시작: {triggerPercent}%\n" +
$"현재 입력 초안 포함" +
currentModelUsageText +
topModelsText +
compactHistory +
compactSession +
postCompactionUsage +
@@ -16718,6 +16741,65 @@ private static (string icon, string label, string bgHex, string fgHex) GetDecisi
PositionThresholdMarker(TokenUsageThresholdMarker, triggerRatio, 18, 18, 14, 3);
}
private static string BuildUsageModelKey(string? service, string? model)
{
var normalizedService = (service ?? "").Trim().ToLowerInvariant();
var normalizedModel = (model ?? "").Trim();
if (string.IsNullOrWhiteSpace(normalizedService) || string.IsNullOrWhiteSpace(normalizedModel))
return "";
return $"{normalizedService}:{normalizedModel}";
}
private static long GetUsageValue(Dictionary<string, long>? source, string key)
{
if (source == null || string.IsNullOrWhiteSpace(key))
return 0;
return source.TryGetValue(key, out var value) ? value : 0;
}
private string BuildTopModelUsageSummary(DailyUsageStats todayUsage)
{
if (todayUsage.ModelPromptTokens.Count == 0 && todayUsage.ModelCompletionTokens.Count == 0)
return "";
var totals = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in todayUsage.ModelPromptTokens)
totals[pair.Key] = pair.Value;
foreach (var pair in todayUsage.ModelCompletionTokens)
totals[pair.Key] = totals.TryGetValue(pair.Key, out var current) ? current + pair.Value : pair.Value;
var topEntries = totals
.OrderByDescending(pair => pair.Value)
.Take(3)
.Select(pair =>
{
var label = pair.Key.Replace(":", " · ");
var postPrompt = GetUsageValue(todayUsage.PostCompactionPromptTokens, pair.Key);
var postCompletion = GetUsageValue(todayUsage.PostCompactionCompletionTokens, pair.Key);
var postTotal = postPrompt + postCompletion;
return postTotal > 0
? $"- {label}: {FormatTokenCount(pair.Value)} tokens (compact 이후 {FormatTokenCount(postTotal)})"
: $"- {label}: {FormatTokenCount(pair.Value)} tokens";
})
.ToList();
return topEntries.Count == 0
? ""
: "\n오늘 상위 모델\n" + string.Join("\n", topEntries);
}
private static string FormatTokenCount(long value)
{
if (value <= int.MaxValue)
return Services.TokenEstimator.Format((int)value);
if (value >= 1_000_000)
return $"{value / 1_000_000d:0.#}M";
if (value >= 1_000)
return $"{value / 1_000d:0.#}K";
return value.ToString("N0");
}
private static void UpdateCircularUsageArc(System.Windows.Shapes.Path path, double ratio, double centerX, double centerY, double radius)
{
ratio = Math.Clamp(ratio, 0, 0.9999);