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) }