Files

120 lines
3.2 KiB
Go

package pricing
import (
"errors"
"fmt"
"math/big"
"strings"
"github.com/nextlevelbuilder/goclaw/internal/providers"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
var ErrUnknownPricing = errors.New("usage pricing unknown")
type BillableUsage struct {
InputTokens int64
OutputTokens int64
CacheReadTokens int64
CacheWriteTokens int64
ReasoningTokens int64
RequestCount int64
ImageCount int64
WebSearchCount int64
}
func FromProviderUsage(u *providers.Usage) BillableUsage {
if u == nil {
return BillableUsage{}
}
inputTokens := int64(u.PromptTokens)
cacheReadTokens := int64(u.CacheReadTokens)
cacheWriteTokens := int64(u.CacheCreationTokens)
if u.PromptTokensIncludeCachedSegments {
inputTokens -= cacheReadTokens + cacheWriteTokens
if inputTokens < 0 {
inputTokens = 0
}
}
return BillableUsage{
InputTokens: inputTokens,
OutputTokens: int64(u.CompletionTokens),
CacheReadTokens: cacheReadTokens,
CacheWriteTokens: cacheWriteTokens,
ReasoningTokens: int64(u.ThinkingTokens),
RequestCount: int64(u.RequestCount),
ImageCount: int64(u.ImageCount),
WebSearchCount: int64(u.WebSearchCount),
}
}
func (u BillableUsage) TotalTokens() int64 {
return u.InputTokens + u.OutputTokens + u.CacheReadTokens + u.CacheWriteTokens
}
func CostMicros(fields store.UsagePricingFields, usage BillableUsage) (int64, error) {
var total int64
add := func(name string, price *string, units int64) error {
if units <= 0 {
return nil
}
if price == nil || strings.TrimSpace(*price) == "" {
return fmt.Errorf("%w: %s", ErrUnknownPricing, name)
}
micros, err := microsForUnits(*price, units)
if err != nil {
return fmt.Errorf("%s pricing: %w", name, err)
}
total += micros
return nil
}
if err := add("input", fields.Input, usage.InputTokens); err != nil {
return 0, err
}
if usage.ReasoningTokens > 0 && fields.Reasoning != nil {
visible := max(usage.OutputTokens-usage.ReasoningTokens, 0)
if err := add("output", fields.Output, visible); err != nil {
return 0, err
}
if err := add("reasoning", fields.Reasoning, usage.ReasoningTokens); err != nil {
return 0, err
}
} else if err := add("output", fields.Output, usage.OutputTokens); err != nil {
return 0, err
}
if err := add("cache_read", fields.CacheRead, usage.CacheReadTokens); err != nil {
return 0, err
}
if err := add("cache_write", fields.CacheWrite, usage.CacheWriteTokens); err != nil {
return 0, err
}
if err := add("request", fields.Request, usage.RequestCount); err != nil {
return 0, err
}
if err := add("image", fields.Image, usage.ImageCount); err != nil {
return 0, err
}
if err := add("web_search", fields.WebSearch, usage.WebSearchCount); err != nil {
return 0, err
}
return total, nil
}
func microsForUnits(price string, units int64) (int64, error) {
r, ok := new(big.Rat).SetString(strings.TrimSpace(price))
if !ok {
return 0, fmt.Errorf("invalid decimal %q", price)
}
r.Mul(r, big.NewRat(units, 1))
r.Mul(r, big.NewRat(1_000_000, 1))
q := new(big.Int).Quo(r.Num(), r.Denom())
rem := new(big.Int).Rem(r.Num(), r.Denom())
if rem.Sign() > 0 {
q.Add(q, big.NewInt(1))
}
if !q.IsInt64() {
return 0, errors.New("cost overflows int64 micros")
}
return q.Int64(), nil
}