multica/server/internal/daemon/usage/scanner.go
Jiayuan 903fbee55d feat(runtimes): add Runtimes tab with usage tracking and connection test
Add a new "Runtimes" sidebar tab to manage local agent runtimes with three
main capabilities: runtime status overview, token usage tracking (reading
Claude Code and Codex CLI local JSONL logs via daemon), and an interactive
connection test that sends a ping through the daemon to verify end-to-end
agent CLI connectivity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 18:28:36 +08:00

68 lines
1.7 KiB
Go

package usage
import (
"log/slog"
)
// Record represents aggregated token usage for one (date, provider, model) tuple.
type Record struct {
Date string `json:"date"` // "2006-01-02"
Provider string `json:"provider"` // "claude" or "codex"
Model string `json:"model"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheWriteTokens int64 `json:"cache_write_tokens"`
}
// Scanner scans local CLI log files for token usage data.
type Scanner struct {
logger *slog.Logger
}
// NewScanner creates a new usage scanner.
func NewScanner(logger *slog.Logger) *Scanner {
return &Scanner{logger: logger}
}
// Scan reads local JSONL log files for both Claude Code and Codex CLI,
// and returns aggregated usage records keyed by (date, provider, model).
func (s *Scanner) Scan() []Record {
var records []Record
claudeRecords := s.scanClaude()
records = append(records, claudeRecords...)
codexRecords := s.scanCodex()
records = append(records, codexRecords...)
return records
}
// aggregation key for merging records.
type aggKey struct {
Date string
Provider string
Model string
}
func mergeRecords(records []Record) []Record {
m := make(map[aggKey]*Record)
for _, r := range records {
k := aggKey{Date: r.Date, Provider: r.Provider, Model: r.Model}
if existing, ok := m[k]; ok {
existing.InputTokens += r.InputTokens
existing.OutputTokens += r.OutputTokens
existing.CacheReadTokens += r.CacheReadTokens
existing.CacheWriteTokens += r.CacheWriteTokens
} else {
copy := r
m[k] = &copy
}
}
result := make([]Record, 0, len(m))
for _, r := range m {
result = append(result, *r)
}
return result
}