Files
llmgw/internal/cli/usage.go
T

127 lines
4.4 KiB
Go

package cli
import (
"encoding/json"
"fmt"
"net/url"
"os"
"text/tabwriter"
"time"
"github.com/spf13/cobra"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
)
// newUsageCmd shows what each group of the tenancy tree spent.
//
// Quotas could be written on an organization, a team or a project long before
// anything could show what any of them had used: usage came back per key, per
// user and per model only. A quota whose consumption cannot be seen is one
// nobody reviews until the day it refuses somebody.
func newUsageExportCmd(client func() *ctlClient) *cobra.Command {
var from, to, period, by, output string
c := &cobra.Command{
Use: "export",
Short: "Usage as CSV, by period and by organization, team, project, user, key or model",
Long: "Totals from the hourly and daily sums, so it reaches back past the request\n" +
"records' retention. Periods are UTC. Cost is given in micro-units and in\n" +
"units; names, not ids, except for keys.",
Example: " llmgw ctl usage export --from 2026-09-01 --to 2026-10-01 --by project,model > september.csv\n" +
" llmgw ctl usage export --period month --by org --output 2026.csv",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
q := url.Values{"period": {period}, "by": {by}, "format": {"json"}}
if from != "" {
q.Set("from", from)
}
if to != "" {
q.Set("to", to)
}
var rows []store.ExportRow
if err := client().get(cmd.Context(), "/api/v1/usage/export?"+q.Encode(), &rows); err != nil {
return err
}
out := cmd.OutOrStdout()
if output != "" {
f, err := os.Create(output)
if err != nil {
return err
}
defer f.Close()
out = f
}
adminapi.WriteExportCSV(out, rows)
if output != "" {
cmd.PrintErrf("%d rows written to %s\n", len(rows), output)
}
return nil
},
}
f := c.Flags()
f.StringVar(&from, "from", "", "first day, 2026-09-01 (default: the first of this month)")
f.StringVar(&to, "to", "", "the day after the last, 2026-10-01 (default: now)")
f.StringVar(&period, "period", "day", "hour, day or month")
f.StringVar(&by, "by", "project,model", "any of org, team, project, user, key, model, comma-separated")
f.StringVarP(&output, "output", "o", "", "write to this file instead of standard output")
return c
}
func newUsageCmd(client func() *ctlClient, asJSON *bool) *cobra.Command {
var by string
var since time.Duration
c := &cobra.Command{
Use: "usage",
Short: "What each organization, team or project has spent",
Long: "Grouped by a level of the tenancy tree, which is the level a quota is\n" +
"written on. Spend is summed across every key beneath it, so two people\n" +
"sharing a team budget appear as one number — the number that will refuse\n" +
"them.\n\n" +
"The budget column is what that level sets, and is blank when it sets none.\n" +
"A team bounded only by its organization has no budget of its own, and\n" +
"printing the organization's beside each of its teams would read as though\n" +
"each had that much to spend.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
var rows []adminapi.ScopeUsageView
path := fmt.Sprintf("/api/v1/usage?by=%s&since=%s", by, since)
if err := client().get(cmd.Context(), path, &rows); err != nil {
return err
}
if *asJSON {
return json.NewEncoder(cmd.OutOrStdout()).Encode(rows)
}
if len(rows) == 0 {
fmt.Fprintf(cmd.OutOrStdout(), "No %s used anything in the last %s.\n", by, since)
return nil
}
w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "NAME\tID\tKEYS\tREQUESTS\tERRORS\tTOKENS\tSPEND\tBUDGET\tUSED\tLAST")
for _, u := range rows {
budget, used := "—", "—"
if u.Budget != nil {
budget = fmt.Sprintf("%.2f", float64(*u.Budget)/1e6)
if u.BudgetPeriod != "" {
budget += "/" + u.BudgetPeriod
}
}
if u.Fraction != nil {
used = fmt.Sprintf("%.0f%%", *u.Fraction*100)
}
fmt.Fprintf(w, "%s\t%s\t%d\t%d\t%d\t%d\t%.2f\t%s\t%s\t%s\n",
u.Name, u.ID, u.Keys, u.Requests, u.Errors,
u.Prompt+u.Completion, float64(u.CostMicros)/1e6,
budget, used, age(u.LastAt))
}
return w.Flush()
},
}
c.Flags().StringVar(&by, "by", "team", "level to group by: org, team or project")
c.Flags().DurationVar(&since, "since", 24*time.Hour, "how far back to total")
c.AddCommand(newUsageExportCmd(client))
return c
}