package llm
import "strings"
// Price is per-million-token list pricing.
type Price struct {
InputPerMTok float64
OutputPerMTok float64
}
// Cache multipliers relative to the input rate.
const (
cacheReadMultiplier = 0.1
cacheWriteMultiplier = 1.25
)
// prices covers the models this harness is likely to be pointed at. A model
// that is not listed reports no cost rather than a wrong one.
var prices = map[string]Price{
"claude-opus-5": {InputPerMTok: 5, OutputPerMTok: 25},
"claude-opus-4-8": {InputPerMTok: 5, OutputPerMTok: 25},
"claude-opus-4-7": {InputPerMTok: 5, OutputPerMTok: 25},
"claude-sonnet-5": {InputPerMTok: 2, OutputPerMTok: 10},
"claude-haiku-4-5": {InputPerMTok: 1, OutputPerMTok: 5},
"claude-fable-5-1": {InputPerMTok: 10, OutputPerMTok: 50},
"claude-fable-5": {InputPerMTok: 10, OutputPerMTok: 50},
}
// Cost prices one usage record. The boolean reports whether the model's
// pricing is known; a made-up number in METRICS.md is worse than a blank.
func (u Usage) Cost(model string) (float64, bool) {
p, ok := prices[basePrice(model)]
if !ok {
return 0, false
}
return (float64(u.InputTokens)*p.InputPerMTok +
float64(u.OutputTokens)*p.OutputPerMTok +
float64(u.CacheReadInputTokens)*p.InputPerMTok*cacheReadMultiplier +
float64(u.CacheCreationInputTokens)*p.InputPerMTok*cacheWriteMultiplier) / 1_000_000, true
}
// PriceOf reports the list pricing for a model, if this harness knows it.
// The picker shows what it knows and says nothing about what it does not.
func PriceOf(model string) (Price, bool) {
p, ok := prices[basePrice(model)]
return p, ok
}
// basePrice strips the decoration the API puts on a model id — a bracketed
// variant tag, a -latest or -YYYYMMDD suffix — leaving the name the price is
// listed under. A dated id and a context-window variant are the same model
// billed at the same rate, and /v1/models hands out both.
func basePrice(model string) string {
if i := strings.IndexByte(model, '['); i >= 0 {
model = model[:i]
}
model = strings.TrimSuffix(model, "-latest")
if i := strings.LastIndexByte(model, '-'); i >= 0 {
if suffix := model[i+1:]; len(suffix) == 8 && allDigits(suffix) {
model = model[:i]
}
}
return model
}
func allDigits(s string) bool {
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return len(s) > 0
}