CI / test (push) Successful in 8m45s
cached_per_1k in a price charges the prompt tokens the answer counts as cached_tokens at their own rate; zero keeps charging them as prompt tokens. The count reaches the cost and not a column: CostMicros holds what it changed. Cached beyond the prompt is clamped, an engine's miscount rather than a refund. BACKLOG L5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
4.0 KiB
Go
122 lines
4.0 KiB
Go
// Package accounting turns finished requests into spend, enforces what a key
|
|
// is allowed, and gets both into the store without touching the request path
|
|
// (plan §7.9.3, §7.9.4).
|
|
package accounting
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Price is what one model costs, in micro-units per thousand tokens.
|
|
//
|
|
// Micro-units rather than a float because spend is accumulated by repeated
|
|
// addition, and repeated addition of floats drifts. What a unit means is the
|
|
// operator's: self-hosted models have no per-token price, so it may be
|
|
// GPU-seconds, a notional rate, or 1000 everywhere — which turns a budget into
|
|
// a token quota.
|
|
type Price struct {
|
|
PromptPer1k int64 `koanf:"prompt_per_1k"`
|
|
CompletionPer1k int64 `koanf:"completion_per_1k"`
|
|
// CachedPer1k is what a prompt token costs that the engine or provider
|
|
// read from its cache instead of computing: the part of prompt_tokens
|
|
// the answer counts as cached_tokens. Zero charges those at
|
|
// PromptPer1k, as before there was a rate for them.
|
|
CachedPer1k int64 `koanf:"cached_per_1k"`
|
|
// PerImage is what one picture costs, for a model that makes them: a
|
|
// picture's cost is the card's time, which its prompt's few tokens do
|
|
// not measure.
|
|
PerImage int64 `koanf:"per_image"`
|
|
// PerMesh is what one 3D model costs: minutes of a card, which no
|
|
// token count measures.
|
|
PerMesh int64 `koanf:"per_mesh"`
|
|
}
|
|
|
|
// Zero reports whether this model spends nothing.
|
|
//
|
|
// It matters beyond arithmetic: a zero-cost model stays available to a key
|
|
// whose budget is spent, which is what makes the plan's budget fallback work
|
|
// (§7.7).
|
|
func (p Price) Zero() bool {
|
|
return p.PromptPer1k == 0 && p.CompletionPer1k == 0 && p.CachedPer1k == 0 && p.PerImage == 0 && p.PerMesh == 0
|
|
}
|
|
|
|
// Cost computes what a request spent, rounding up.
|
|
//
|
|
// Up rather than to nearest: rounding down would make a stream of tiny requests
|
|
// free, which is exactly the shape of traffic a quota exists to bound.
|
|
//
|
|
// cachedTokens are counted within promptTokens, as OpenAI's usage counts
|
|
// them, and are charged at the cached rate instead of the prompt rate.
|
|
func (p Price) Cost(promptTokens, cachedTokens, completionTokens, images, meshes int64) int64 {
|
|
cachedTokens = min(max(cachedTokens, 0), max(promptTokens, 0))
|
|
cachedRate := p.CachedPer1k
|
|
if cachedRate == 0 {
|
|
cachedRate = p.PromptPer1k
|
|
}
|
|
return divUp((promptTokens-cachedTokens)*p.PromptPer1k+cachedTokens*cachedRate, 1000) +
|
|
divUp(completionTokens*p.CompletionPer1k, 1000) +
|
|
max(images, 0)*p.PerImage + max(meshes, 0)*p.PerMesh
|
|
}
|
|
|
|
func divUp(n, d int64) int64 {
|
|
if n <= 0 {
|
|
return 0
|
|
}
|
|
return (n + d - 1) / d
|
|
}
|
|
|
|
// Pricing resolves a model name to its price.
|
|
type Pricing struct {
|
|
mu sync.RWMutex
|
|
byModel map[string]Price
|
|
fallback Price
|
|
}
|
|
|
|
// NewPricing builds a price table. Names are matched case-insensitively,
|
|
// because a model name reaches us from whatever the client typed.
|
|
func NewPricing(byModel map[string]Price, fallback Price) *Pricing {
|
|
table := make(map[string]Price, len(byModel))
|
|
for name, p := range byModel {
|
|
table[strings.ToLower(name)] = p
|
|
}
|
|
return &Pricing{byModel: table, fallback: fallback}
|
|
}
|
|
|
|
// For returns a model's price, or the fallback.
|
|
func (p *Pricing) For(model string) Price {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
if v, ok := p.byModel[strings.ToLower(model)]; ok {
|
|
return v
|
|
}
|
|
return p.fallback
|
|
}
|
|
|
|
// AnythingPriced reports whether any model costs anything.
|
|
//
|
|
// A budget is spend per period, and spend is computed from a price table. With
|
|
// no prices every request costs nothing, so a budget written on a team is a
|
|
// number that can never move — displayed as a limit, reported as zero percent
|
|
// used, and binding on nothing.
|
|
//
|
|
// Self-hosted models have no per-token price, so an empty table is the ordinary
|
|
// case rather than a misconfiguration. What is not ordinary is showing somebody
|
|
// a budget without saying so.
|
|
func (p *Pricing) AnythingPriced() bool {
|
|
if p == nil {
|
|
return false
|
|
}
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
if !p.fallback.Zero() {
|
|
return true
|
|
}
|
|
for _, price := range p.byModel {
|
|
if !price.Zero() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|