Vectrum
Sign In
124 lines · 4.2 KB · Go
package llm

import (
	"sort"
	"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 {
	// Gateways decorate the id: a routing prefix (openrouter/anthropic/...),
	// a Bedrock namespace (anthropic.claude-opus-5), a Vertex version suffix
	// (claude-opus-5@20260401). They all name the same model, and a lookup
	// that cannot see past them prices nothing and, worse, answers a
	// capability question wrongly for every request through a proxy.
	if i := strings.LastIndexByte(model, '/'); i >= 0 {
		model = model[i+1:]
	}
	model = strings.TrimPrefix(model, "anthropic.")
	if i := strings.IndexByte(model, '@'); i >= 0 {
		model = model[:i]
	}
	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
}

// PricedModels are the ids this build has list pricing for, flagship first and
// cheapest last within a family.
//
// It exists for the setup wizard, which has to offer a usable list even when
// the endpoint will not serve /v1/models — a gateway that only proxies
// /v1/messages is the common case, and a first run cannot be left with nothing
// to pick. The set it can price is the set it can fully describe, which is
// what makes this the honest fallback rather than a second hardcoded list.
func PricedModels() []string {
	out := make([]string, 0, len(prices))
	for id := range prices {
		out = append(out, id)
	}
	// Map order is random; a picker has to look the same every time.
	sort.Slice(out, func(i, j int) bool {
		if ri, rj := familyRank(out[i]), familyRank(out[j]); ri != rj {
			return ri < rj
		}
		return out[i] > out[j] // newer version first within a family
	})
	return out
}

// familyRank orders the families the way someone choosing between them does:
// the flagship first, the cheap one last, anything unrecognised after both.
func familyRank(id string) int {
	for i, family := range []string{"claude-opus", "claude-sonnet", "claude-haiku", "claude-fable"} {
		if strings.HasPrefix(id, family) {
			return i
		}
	}
	// Unrecognised: after every known family, and left to the id tiebreak to
	// order among themselves.
	return len(prices)
}