The account page (was "Your keys") shows the last thirty days of the person's own use - requests, tokens, spend and tokens per day - and, for each place they can spend, what they used there and a bar for every budget that applies: their own, a per-member one, or a project's, team's or organization's shared with others, named as such. Bars turn amber past four-fifths and red when spent. It sits under the keys now, above the password form. Spend is read from the usage table for the pool each limit counts against, from the start of its period. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
138 lines
3.8 KiB
Go
138 lines
3.8 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/accounting"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
// SelfUsage is what a person's own page reads of their use. Optional: a
|
|
// store without it leaves the page showing limits alone.
|
|
type SelfUsage interface {
|
|
UsageOfUser(ctx context.Context, userID string, since time.Time, width time.Duration) ([]store.PlaceUsage, []store.Bucket, error)
|
|
SpendOfPool(ctx context.Context, l store.Limits, since time.Time) (int64, error)
|
|
}
|
|
|
|
// selfWindow is how far back a person's own page looks.
|
|
const selfWindow = 30 * 24 * time.Hour
|
|
|
|
// ownUse is the person's last thirty days, for the top of their page.
|
|
type ownUse struct {
|
|
Requests, Tokens int64
|
|
Spend string
|
|
Chart template.HTML
|
|
}
|
|
|
|
// budgetBar is one budget that applies, and how much of it is gone.
|
|
type budgetBar struct {
|
|
// Label says whose pool it is: the person's own, or one they share.
|
|
Label string
|
|
Used, Budget string
|
|
Period string
|
|
Percent int
|
|
// Near is past four-fifths, when it is worth colouring.
|
|
Near, Over bool
|
|
}
|
|
|
|
// placeUse is what a person spent in one place over the window.
|
|
type placeUse struct {
|
|
Requests, Tokens int64
|
|
Spend string
|
|
}
|
|
|
|
func (p *AccountPages) fillOwnUse(ctx context.Context, user store.User, d *profileData) {
|
|
su, ok := p.keys.(SelfUsage)
|
|
if !ok {
|
|
return
|
|
}
|
|
places, buckets, err := su.UsageOfUser(ctx, user.ID, time.Now().Add(-selfWindow), 24*time.Hour)
|
|
if err != nil {
|
|
p.log.Warn("could not read a person's own usage", "err", err)
|
|
return
|
|
}
|
|
u := &ownUse{}
|
|
var cost int64
|
|
byPlace := map[string]placeUse{}
|
|
for _, pl := range places {
|
|
u.Requests += pl.Requests
|
|
u.Tokens += pl.Tokens
|
|
cost += pl.CostMicros
|
|
byPlace[pl.ProjectID] = placeUse{pl.Requests, pl.Tokens, units(pl.CostMicros)}
|
|
}
|
|
u.Spend = units(cost)
|
|
s := Series{Colour: "--accent"}
|
|
for _, b := range buckets {
|
|
s.Values = append(s.Values, float64(b.Prompt+b.Completion))
|
|
s.Labels = append(s.Labels, b.Start.UTC().Format("Jan 2"))
|
|
}
|
|
if u.Requests > 0 {
|
|
u.Chart = BarChart(s, "tokens")
|
|
}
|
|
d.Own = u
|
|
|
|
for i := range d.LimitsBy {
|
|
c := &d.LimitsBy[i]
|
|
if pu, ok := byPlace[c.project]; ok {
|
|
c.Use = &pu
|
|
}
|
|
for _, l := range c.chain {
|
|
if l.Budget == nil || l.Scope == store.ScopeKey {
|
|
continue
|
|
}
|
|
start := accounting.PeriodStart(l.BudgetPeriod, time.Now())
|
|
spent, err := su.SpendOfPool(ctx, l, start)
|
|
if err != nil {
|
|
p.log.Warn("could not read a pool's spend", "scope", l.Scope, "err", err)
|
|
continue
|
|
}
|
|
bar := budgetBar{Label: poolLabel(l.Scope), Used: units(spent), Budget: units(*l.Budget),
|
|
Period: periodWord(l.BudgetPeriod)}
|
|
if *l.Budget > 0 {
|
|
bar.Percent = int(min(spent*100 / *l.Budget, 100))
|
|
bar.Near = spent*5 >= *l.Budget*4
|
|
bar.Over = spent >= *l.Budget
|
|
}
|
|
c.Budgets = append(c.Budgets, bar)
|
|
}
|
|
}
|
|
}
|
|
|
|
// poolLabel says whose budget a limit is, which is the thing a person needs
|
|
// to read a bar right: their own is theirs to spend, a project's is shared
|
|
// with everyone in it.
|
|
func poolLabel(s store.Scope) string {
|
|
switch s {
|
|
case store.ScopeUser:
|
|
return "Your budget, across everything you use"
|
|
case store.ScopeProjectMember:
|
|
return "Your budget in this project"
|
|
case store.ScopeTeamMember:
|
|
return "Your budget in this team"
|
|
case store.ScopeOrgMember:
|
|
return "Your budget in this organization"
|
|
case store.ScopeProject:
|
|
return "The project's budget, shared with its members"
|
|
case store.ScopeTeam:
|
|
return "The team's budget, shared across its projects"
|
|
case store.ScopeOrg:
|
|
return "The organization's budget, shared across it"
|
|
}
|
|
return string(s)
|
|
}
|
|
|
|
func periodWord(p store.Period) string {
|
|
switch p {
|
|
case store.PeriodDay:
|
|
return "today"
|
|
case store.PeriodMonth:
|
|
return "this month"
|
|
case store.PeriodNone:
|
|
return "in total"
|
|
}
|
|
return fmt.Sprintf("this %s", p)
|
|
}
|