package web import ( "context" "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, l *Lang, 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, l.Units(pl.CostMicros)} } u.Spend = l.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, l.Format(b.Start.UTC(), "Jan 2")) } if u.Requests > 0 { u.Chart = l.BarChart(s, l.T("selfuse.chart_unit")) } d.Own = u for i := range d.LimitsBy { c := &d.LimitsBy[i] if pu, ok := byPlace[c.project]; ok { c.Use = &pu } for _, lim := range c.chain { if lim.Budget == nil || lim.Scope == store.ScopeKey { continue } start := accounting.PeriodStart(lim.BudgetPeriod, time.Now()) spent, err := su.SpendOfPool(ctx, lim, start) if err != nil { p.log.Warn("could not read a pool's spend", "scope", lim.Scope, "err", err) continue } bar := budgetBar{Label: poolLabel(l, lim.Scope), Used: l.Units(spent), Budget: l.Units(*lim.Budget), Period: periodWord(l, lim.BudgetPeriod)} if *lim.Budget > 0 { bar.Percent = int(min(spent*100 / *lim.Budget, 100)) bar.Near = spent*5 >= *lim.Budget*4 bar.Over = spent >= *lim.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(l *Lang, s store.Scope) string { switch s { case store.ScopeUser: return l.T("selfuse.pool.user") case store.ScopeProjectMember: return l.T("selfuse.pool.project_member") case store.ScopeTeamMember: return l.T("selfuse.pool.team_member") case store.ScopeOrgMember: return l.T("selfuse.pool.org_member") case store.ScopeProject: return l.T("selfuse.pool.project") case store.ScopeTeam: return l.T("selfuse.pool.team") case store.ScopeOrg: return l.T("selfuse.pool.org") } return string(s) } func periodWord(l *Lang, p store.Period) string { switch p { case store.PeriodDay: return l.T("selfuse.period.day") case store.PeriodMonth: return l.T("selfuse.period.month") case store.PeriodNone: return l.T("selfuse.period.none") } return l.T("selfuse.period.other", string(p)) }