Files
MuyueWorkspace/internal/api/consumption.go
Augustin cb3d35756a
All checks were successful
Beta Release / beta (push) Successful in 1m3s
feat: terminal sudo blocking, token tracking, mermaid & consumption UI
- Block sudo/doas commands when not running as root
- Add real token counting from API responses
- Track and display consumption by provider/day
- Add Mermaid diagram rendering in Shell and Studio
- Add copy-to-clipboard buttons for code blocks
- Support tables in AI message rendering
- Update system prompt with context (date, time, root status)

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
2026-04-26 12:43:15 +02:00

128 lines
2.5 KiB
Go

package api
import (
"encoding/json"
"os"
"path/filepath"
"sync"
"time"
"github.com/muyue/muyue/internal/config"
)
type consumptionEntry struct {
Date string `json:"date"`
Tokens int `json:"tokens"`
Requests int `json:"requests"`
}
type providerConsumption struct {
Name string `json:"name"`
Daily []consumptionEntry `json:"daily"`
Total int `json:"total_tokens"`
Requests int `json:"total_requests"`
}
type consumptionStore struct {
mu sync.Mutex
providers map[string]*providerConsumption
}
func newConsumptionStore() *consumptionStore {
cs := &consumptionStore{
providers: make(map[string]*providerConsumption),
}
cs.load()
cs.prune()
return cs
}
func (cs *consumptionStore) Record(providerName string, tokens int) {
if tokens <= 0 || providerName == "" {
return
}
cs.mu.Lock()
defer cs.mu.Unlock()
today := time.Now().UTC().Format("2006-01-02")
p, ok := cs.providers[providerName]
if !ok {
p = &providerConsumption{Name: providerName}
cs.providers[providerName] = p
}
p.Total += tokens
p.Requests++
if len(p.Daily) > 0 && p.Daily[len(p.Daily)-1].Date == today {
p.Daily[len(p.Daily)-1].Tokens += tokens
p.Daily[len(p.Daily)-1].Requests++
} else {
p.Daily = append(p.Daily, consumptionEntry{
Date: today,
Tokens: tokens,
Requests: 1,
})
}
cs.save()
}
func (cs *consumptionStore) GetAll() map[string]*providerConsumption {
cs.mu.Lock()
defer cs.mu.Unlock()
result := make(map[string]*providerConsumption)
for k, v := range cs.providers {
pc := *v
daily := make([]consumptionEntry, len(v.Daily))
copy(daily, v.Daily)
pc.Daily = daily
result[k] = &pc
}
return result
}
func (cs *consumptionStore) prune() {
cutoff := time.Now().UTC().AddDate(0, 0, -7).Format("2006-01-02")
for _, p := range cs.providers {
filtered := make([]consumptionEntry, 0)
for _, d := range p.Daily {
if d.Date >= cutoff {
filtered = append(filtered, d)
}
}
p.Daily = filtered
}
}
func (cs *consumptionStore) filePath() string {
dir, err := config.ConfigDir()
if err != nil {
return ""
}
return filepath.Join(dir, "consumption.json")
}
func (cs *consumptionStore) load() {
fp := cs.filePath()
if fp == "" {
return
}
data, err := os.ReadFile(fp)
if err != nil {
return
}
json.Unmarshal(data, &cs.providers)
}
func (cs *consumptionStore) save() {
fp := cs.filePath()
if fp == "" {
return
}
data, _ := json.Marshal(cs.providers)
os.WriteFile(fp, data, 0644)
}