chore: add TokenLens sources and ignore rules
This commit is contained in:
330
NativeTokenLens/Sources/TokenLensCore/UsageSummary.swift
Normal file
330
NativeTokenLens/Sources/TokenLensCore/UsageSummary.swift
Normal file
@@ -0,0 +1,330 @@
|
||||
import Foundation
|
||||
|
||||
public struct UsageSummaryAccumulator {
|
||||
private let now: Date
|
||||
private var calendar: Calendar
|
||||
private let today: Date
|
||||
private let month: DateComponents
|
||||
private let trendWindows: [TrendMode: TrendWindow]
|
||||
|
||||
private var providerTotals: [Provider: TokenBucket]
|
||||
private var todayProviderTotals: [Provider: TokenBucket]
|
||||
private var dailyBuckets: [Date: [Provider: Int]] = [:]
|
||||
private var dailyDetails: [Date: DailyUsageDetail] = [:]
|
||||
private var trendBuckets: [TrendMode: [Date: [Provider: Int]]] = [:]
|
||||
private var todayTokens = 0
|
||||
private var monthTokens = 0
|
||||
private var totalTokens = 0
|
||||
private var cachedTokens = 0
|
||||
private var sessions = Set<String>()
|
||||
|
||||
public init(
|
||||
now: Date = Date(),
|
||||
calendar baseCalendar: Calendar = Calendar.current
|
||||
) {
|
||||
self.now = now
|
||||
var calendar = baseCalendar
|
||||
calendar.firstWeekday = 2
|
||||
self.calendar = calendar
|
||||
today = calendar.startOfDay(for: now)
|
||||
month = calendar.dateComponents([.year, .month], from: now)
|
||||
providerTotals = Dictionary(uniqueKeysWithValues: Provider.allCases.map {
|
||||
($0, TokenBucket(provider: $0))
|
||||
})
|
||||
todayProviderTotals = Dictionary(uniqueKeysWithValues: Provider.allCases.map {
|
||||
($0, TokenBucket(provider: $0))
|
||||
})
|
||||
trendWindows = Dictionary(uniqueKeysWithValues: TrendMode.allCases.map { mode in
|
||||
let reference = mode.periodStart(for: now, calendar: calendar)
|
||||
return (mode, TrendWindow(
|
||||
reference: reference,
|
||||
start: mode.startDate(reference: reference, calendar: calendar),
|
||||
component: mode.calendarComponent
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
public mutating func add(_ record: UsageRecord) {
|
||||
add(
|
||||
provider: record.provider,
|
||||
sourcePath: record.sourcePath,
|
||||
timestamp: record.timestamp,
|
||||
sessionID: record.sessionID,
|
||||
inputTokens: record.inputTokens,
|
||||
outputTokens: record.outputTokens,
|
||||
cachedTokens: record.cachedTokens,
|
||||
reasoningTokens: record.reasoningTokens,
|
||||
toolTokens: record.toolTokens,
|
||||
totalTokens: record.totalTokens
|
||||
)
|
||||
}
|
||||
|
||||
public mutating func add(
|
||||
provider: Provider,
|
||||
sourcePath: String,
|
||||
timestamp: Date,
|
||||
sessionID: String,
|
||||
inputTokens: Int,
|
||||
outputTokens: Int,
|
||||
cachedTokens: Int,
|
||||
reasoningTokens: Int,
|
||||
toolTokens: Int,
|
||||
totalTokens: Int
|
||||
) {
|
||||
self.totalTokens += totalTokens
|
||||
self.cachedTokens += cachedTokens
|
||||
sessions.insert(sessionID.isEmpty ? sourcePath : sessionID)
|
||||
providerTotals[provider, default: TokenBucket(provider: provider)].add(
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cachedTokens: cachedTokens,
|
||||
reasoningTokens: reasoningTokens,
|
||||
toolTokens: toolTokens,
|
||||
totalTokens: totalTokens
|
||||
)
|
||||
|
||||
let recordDay = calendar.startOfDay(for: timestamp)
|
||||
dailyBuckets[recordDay, default: Provider.zeroTokenMap][provider, default: 0] += totalTokens
|
||||
if dailyDetails[recordDay] == nil {
|
||||
dailyDetails[recordDay] = DailyUsageDetail(date: recordDay)
|
||||
}
|
||||
dailyDetails[recordDay]?.add(
|
||||
provider: provider,
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cachedTokens: cachedTokens,
|
||||
totalTokens: totalTokens
|
||||
)
|
||||
|
||||
if recordDay == today {
|
||||
todayTokens += totalTokens
|
||||
todayProviderTotals[provider, default: TokenBucket(provider: provider)].add(
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cachedTokens: cachedTokens,
|
||||
reasoningTokens: reasoningTokens,
|
||||
toolTokens: toolTokens,
|
||||
totalTokens: totalTokens
|
||||
)
|
||||
}
|
||||
|
||||
let recordMonth = calendar.dateComponents([.year, .month], from: timestamp)
|
||||
if recordMonth.year == month.year && recordMonth.month == month.month {
|
||||
monthTokens += totalTokens
|
||||
}
|
||||
|
||||
for mode in TrendMode.allCases {
|
||||
guard let window = trendWindows[mode] else { continue }
|
||||
let period = mode.periodStart(for: timestamp, calendar: calendar)
|
||||
guard period >= window.start && period <= window.reference else { continue }
|
||||
trendBuckets[mode, default: [:]][period, default: Provider.zeroTokenMap][provider, default: 0] += totalTokens
|
||||
}
|
||||
}
|
||||
|
||||
public func makeSummary() -> UsageSummary {
|
||||
let dailyTrend = dailyBuckets
|
||||
.sorted { $0.key < $1.key }
|
||||
.suffix(30)
|
||||
.map { DailyPoint(date: $0.key, values: $0.value) }
|
||||
|
||||
let sortedDailyDetails = dailyDetails.values.sorted { $0.date > $1.date }
|
||||
let trendPointsByMode = Dictionary(uniqueKeysWithValues: TrendMode.allCases.map { mode in
|
||||
(mode, makeTrendPoints(mode: mode))
|
||||
})
|
||||
|
||||
let toolRows = Provider.allCases.compactMap { provider -> ToolRow? in
|
||||
guard let bucket = providerTotals[provider], bucket.totalTokens > 0 else { return nil }
|
||||
let trendTokens = dailyTrend.map { $0.values[provider, default: 0] }
|
||||
return ToolRow(
|
||||
provider: provider,
|
||||
inputTokens: bucket.inputTokens,
|
||||
outputTokens: bucket.outputTokens,
|
||||
cachedTokens: bucket.cachedTokens,
|
||||
totalTokens: bucket.totalTokens,
|
||||
trendTokens: trendTokens
|
||||
)
|
||||
}
|
||||
.sorted { $0.totalTokens > $1.totalTokens }
|
||||
|
||||
let cacheHitRate = totalTokens > 0 ? Int((Double(cachedTokens) / Double(totalTokens) * 100).rounded()) : 0
|
||||
let cards = SummaryCards(
|
||||
todayTokens: todayTokens,
|
||||
monthTokens: monthTokens,
|
||||
totalTokens: totalTokens,
|
||||
cacheHitRate: cacheHitRate,
|
||||
sessionCount: sessions.count
|
||||
)
|
||||
|
||||
return UsageSummary(
|
||||
generatedAt: now,
|
||||
records: [],
|
||||
cards: cards,
|
||||
providerTotals: providerTotals,
|
||||
todayProviderTotals: todayProviderTotals,
|
||||
dailyTrend: Array(dailyTrend),
|
||||
trendPointsByMode: trendPointsByMode,
|
||||
dailyDetails: sortedDailyDetails,
|
||||
toolRows: toolRows,
|
||||
insights: UsageSummary.makeInsights(
|
||||
providerTotals: providerTotals,
|
||||
todayTokens: todayTokens,
|
||||
cacheHitRate: cacheHitRate
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func makeTrendPoints(mode: TrendMode) -> [DailyPoint] {
|
||||
guard let window = trendWindows[mode] else { return [] }
|
||||
let buckets = trendBuckets[mode, default: [:]]
|
||||
var points: [DailyPoint] = []
|
||||
var cursor = window.start
|
||||
while cursor <= window.reference {
|
||||
points.append(DailyPoint(date: cursor, values: buckets[cursor, default: Provider.zeroTokenMap]))
|
||||
guard let next = calendar.date(byAdding: window.component, value: 1, to: cursor) else { break }
|
||||
cursor = next
|
||||
}
|
||||
return points
|
||||
}
|
||||
}
|
||||
|
||||
private struct TrendWindow {
|
||||
let reference: Date
|
||||
let start: Date
|
||||
let component: Calendar.Component
|
||||
}
|
||||
|
||||
public extension UsageSummary {
|
||||
static func make(
|
||||
records: [UsageRecord],
|
||||
now: Date = Date(),
|
||||
calendar baseCalendar: Calendar = Calendar.current
|
||||
) -> UsageSummary {
|
||||
var accumulator = UsageSummaryAccumulator(now: now, calendar: baseCalendar)
|
||||
for record in records {
|
||||
accumulator.add(record)
|
||||
}
|
||||
return accumulator.makeSummary()
|
||||
}
|
||||
|
||||
static func makeInsights(
|
||||
providerTotals: [Provider: TokenBucket],
|
||||
todayTokens: Int,
|
||||
cacheHitRate: Int
|
||||
) -> [Insight] {
|
||||
let top = providerTotals.values
|
||||
.filter { $0.totalTokens > 0 }
|
||||
.sorted { $0.totalTokens > $1.totalTokens }
|
||||
.first
|
||||
|
||||
var insights: [Insight] = []
|
||||
if let top, let provider = top.provider {
|
||||
insights.append(Insight(
|
||||
tone: .warning,
|
||||
title: "\(provider.rawValue) 是当前主要用量来源",
|
||||
message: "已从本地日志统计 \(TokenFormatter.format(top.totalTokens)) Tokens。"
|
||||
))
|
||||
}
|
||||
insights.append(Insight(
|
||||
tone: .positive,
|
||||
title: "检测到缓存 Tokens",
|
||||
message: "当前 \(cacheHitRate)% 的统计用量来自缓存上下文。"
|
||||
))
|
||||
insights.append(Insight(
|
||||
tone: .accent,
|
||||
title: "今日用量快照已更新",
|
||||
message: "当前本地日期已统计 \(TokenFormatter.format(todayTokens)) Tokens。"
|
||||
))
|
||||
return insights
|
||||
}
|
||||
|
||||
func trendPoints(
|
||||
mode: TrendMode,
|
||||
calendar baseCalendar: Calendar = Calendar.current
|
||||
) -> [DailyPoint] {
|
||||
if let points = trendPointsByMode[mode] {
|
||||
return points
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
func dailyUsageDetails(calendar baseCalendar: Calendar = Calendar.current) -> [DailyUsageDetail] {
|
||||
dailyDetails
|
||||
}
|
||||
|
||||
func updatingGeneratedAt(_ date: Date) -> UsageSummary {
|
||||
UsageSummary(
|
||||
generatedAt: date,
|
||||
records: records,
|
||||
cards: cards,
|
||||
providerTotals: providerTotals,
|
||||
todayProviderTotals: todayProviderTotals,
|
||||
dailyTrend: dailyTrend,
|
||||
trendPointsByMode: trendPointsByMode,
|
||||
dailyDetails: dailyDetails,
|
||||
toolRows: toolRows,
|
||||
insights: insights
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Provider {
|
||||
static var zeroTokenMap: [Provider: Int] {
|
||||
Dictionary(uniqueKeysWithValues: Provider.allCases.map { ($0, 0) })
|
||||
}
|
||||
}
|
||||
|
||||
extension TrendMode {
|
||||
var calendarComponent: Calendar.Component {
|
||||
switch self {
|
||||
case .daily:
|
||||
return .hour
|
||||
case .weekly:
|
||||
return .day
|
||||
case .monthly:
|
||||
return .day
|
||||
case .yearly:
|
||||
return .month
|
||||
}
|
||||
}
|
||||
|
||||
var periodColumn: String {
|
||||
switch self {
|
||||
case .daily:
|
||||
return "hour_start"
|
||||
case .weekly, .monthly:
|
||||
return "day_start"
|
||||
case .yearly:
|
||||
return "month_start"
|
||||
}
|
||||
}
|
||||
|
||||
func periodStart(for date: Date, calendar: Calendar) -> Date {
|
||||
switch self {
|
||||
case .daily:
|
||||
let components = calendar.dateComponents([.year, .month, .day, .hour], from: date)
|
||||
return calendar.date(from: components) ?? calendar.startOfDay(for: date)
|
||||
case .weekly:
|
||||
return calendar.startOfDay(for: date)
|
||||
case .monthly:
|
||||
return calendar.startOfDay(for: date)
|
||||
case .yearly:
|
||||
let components = calendar.dateComponents([.year, .month], from: date)
|
||||
return calendar.date(from: components) ?? calendar.startOfDay(for: date)
|
||||
}
|
||||
}
|
||||
|
||||
func startDate(reference: Date, calendar: Calendar) -> Date {
|
||||
switch self {
|
||||
case .daily:
|
||||
return calendar.startOfDay(for: reference)
|
||||
case .weekly:
|
||||
return calendar.dateInterval(of: .weekOfYear, for: reference)?.start ?? calendar.startOfDay(for: reference)
|
||||
case .monthly:
|
||||
let components = calendar.dateComponents([.year, .month], from: reference)
|
||||
return calendar.date(from: components) ?? calendar.startOfDay(for: reference)
|
||||
case .yearly:
|
||||
let components = calendar.dateComponents([.year], from: reference)
|
||||
return calendar.date(from: components) ?? calendar.startOfDay(for: reference)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user