Files
llmgw/internal/cli/ctl_prices.go
T
Ilker ManapandClaude Opus 5.5 8547528fb7
CI / test (push) Successful in 9m51s
Prices from what the cluster costs: ctl prices suggest
auth.gpu_hour_cost (and core_hour_cost) is what an hour of a GPU (a core)
costs the operator. The leader keeps, by the hour, the GPU- and core-hours
each model's replicas hold (held_1h); llmgw ctl prices suggest spreads a
model's over the tokens it served, an output token counted as four input
ones and a cached prompt token as a tenth of one, and --apply writes the
prices into auth.pricing. Idle hours are in the cost, so a model held and
little used comes out dear; one held and billed nothing is shown with what
it held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-15 06:54:55 +01:00

122 lines
4.6 KiB
Go

package cli
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"text/tabwriter"
"github.com/spf13/cobra"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi"
"gitea.mynodes.xyz/ilker/llmgw/internal/settings"
)
func newPricesCmd(client func() *ctlClient, asJSON *bool) *cobra.Command {
cmd := &cobra.Command{
Use: "prices",
Short: "What each model's tokens cost, from what the cluster costs",
}
var days int
var apply bool
var note string
suggest := &cobra.Command{
Use: "suggest",
Short: "Each model's price from the GPU- and core-hours it held, over the tokens it served",
Long: "Set auth.gpu_hour_cost first: what an hour of one GPU costs, in the units budgets are in (the\n" +
"hardware written off over its years, power and room, the people who run it, over the GPUs and\n" +
"the hours of a month); auth.core_hour_cost likewise for models on CPUs. Each model's cost is\n" +
"the hours its replicas held, idle ones too, kept by the hour on the leader; spread over its\n" +
"tokens, an output token counted as four input ones and a cached prompt token as a tenth of one,\n" +
"it is its price. A model held and not used shows what that cost. --apply writes the prices\n" +
"into auth.pricing, one settings version, as `ctl config set` does.",
Example: " llmgw ctl config set auth.gpu_hour_cost 1.35\n" +
" llmgw ctl prices suggest --days 30\n" +
" llmgw ctl prices suggest --apply --note \"November's costs\"",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
var s adminapi.PriceSuggestions
if err := client().get(cmd.Context(), "/api/v1/prices/suggest?days="+url.QueryEscape(strconv.Itoa(days)), &s); err != nil {
return err
}
w := cmd.OutOrStdout()
if *asJSON {
return json.NewEncoder(w).Encode(s)
}
fmt.Fprintf(w, "cost: %g a GPU-hour, %g a core-hour; the last %d days", s.GPUHourCost, s.CoreHourCost, s.Days)
if !s.RecordedFrom.IsZero() && s.RecordedFrom.After(s.Since) {
fmt.Fprintf(w, " (held kept only since %s)", s.RecordedFrom.Local().Format("2006-01-02 15:04"))
}
fmt.Fprintln(w)
fmt.Fprintln(w, "prices a million tokens: input / output / cached input, in budget units")
tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
fmt.Fprintln(tw, "MODEL\tGPU-H\tCORE-H\tCOST\tTOKENS IN/OUT\tSUGGESTED\tNOW\tNOTE")
var changed []adminapi.PriceSuggestion
for _, m := range s.Models {
sug := "-"
if !m.Price.Zero() {
sug = perMillion(m.Price.PromptPer1k, m.Price.CompletionPer1k, m.Price.CachedPer1k)
if m.Price.PromptPer1k != m.Current.PromptPer1k || m.Price.CompletionPer1k != m.Current.CompletionPer1k ||
m.Price.CachedPer1k != m.Current.CachedPer1k {
changed = append(changed, m)
}
}
fmt.Fprintf(tw, "%s\t%.1f\t%.1f\t%.2f\t%s/%s\t%s\t%s\t%s\n", m.Model, m.GPUHours, m.CoreHours, m.Cost,
count(m.Prompt), count(m.Completion), sug,
perMillion(m.Current.PromptPer1k, m.Current.CompletionPer1k, m.Current.CachedPer1k), m.Note)
}
if err := tw.Flush(); err != nil {
return err
}
if !apply {
if len(changed) > 0 {
fmt.Fprintf(w, "%d model(s) priced otherwise now; --apply writes the suggestions\n", len(changed))
}
return nil
}
if len(changed) == 0 {
fmt.Fprintln(w, "nothing to change")
return nil
}
return changeSettings(cmd, client, note, func(doc *settings.Doc) error {
for _, m := range changed {
for k, v := range map[string]int64{"prompt_per_1k": m.Price.PromptPer1k,
"completion_per_1k": m.Price.CompletionPer1k, "cached_per_1k": m.Price.CachedPer1k} {
if err := doc.SetYAML([]string{"auth", "pricing", m.Model, k}, strconv.FormatInt(v, 10)); err != nil {
return err
}
}
}
return nil
})
},
}
suggest.Flags().IntVar(&days, "days", 30, "the window, in days")
suggest.Flags().BoolVar(&apply, "apply", false, "write the suggested prices into auth.pricing")
suggest.Flags().StringVar(&note, "note", "", "why, kept with the settings' version")
cmd.AddCommand(suggest)
return cmd
}
// perMillion is micro-units a thousand tokens as budget units a million.
func perMillion(in, out, cached int64) string {
if in == 0 && out == 0 && cached == 0 {
return "-"
}
f := func(v int64) string { return strconv.FormatFloat(float64(v)/1000, 'f', -1, 64) }
return f(in) + " / " + f(out) + " / " + f(cached)
}
func count(n int64) string {
switch {
case n >= 1e9:
return fmt.Sprintf("%.1fB", float64(n)/1e9)
case n >= 1e6:
return fmt.Sprintf("%.1fM", float64(n)/1e6)
case n >= 1e3:
return fmt.Sprintf("%.1fk", float64(n)/1e3)
}
return strconv.FormatInt(n, 10)
}