mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-04 11:35:11 -04:00
157 lines
5.8 KiB
Go
157 lines
5.8 KiB
Go
// Package pricing builds the default LLM pricing table the synthesizer
|
|
// ships to the proxy's cost_meter middleware. The catalog is the single
|
|
// source of default rates: every catalog provider's models are folded
|
|
// into the pricing surfaces the provider declares (PricingSurfaces),
|
|
// then a small supplemental list adds priced-but-not-operator-selectable
|
|
// entries. Management is the sole pricing authority — the proxy carries
|
|
// no embedded price list and bills exclusively from the table it is
|
|
// sent.
|
|
//go:generate go run gen.go
|
|
|
|
package pricing
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/catalog"
|
|
)
|
|
|
|
// Entry is a single model's pricing in USD per 1k tokens. This struct IS
|
|
// the wire shape: the synthesizer marshals it verbatim into cost_meter's
|
|
// ConfigJSON, and the proxy unmarshals the same field names.
|
|
//
|
|
// A zero rate means "no rate configured" — the proxy bills that cache
|
|
// bucket at InputPer1k (identical semantics to the retired proxy-embedded
|
|
// table). CachedInputPer1k is the OpenAI shape (cached prompt tokens are
|
|
// a subset of input); CacheReadPer1k / CacheCreationPer1k are the
|
|
// Anthropic shape (additive buckets).
|
|
type Entry struct {
|
|
InputPer1k float64 `json:"input_per_1k"`
|
|
OutputPer1k float64 `json:"output_per_1k"`
|
|
CachedInputPer1k float64 `json:"cached_input_per_1k,omitempty"`
|
|
CacheReadPer1k float64 `json:"cache_read_per_1k,omitempty"`
|
|
CacheCreationPer1k float64 `json:"cache_creation_per_1k,omitempty"`
|
|
}
|
|
|
|
// supplementalDefaults are (surface, model) entries that are priced but
|
|
// deliberately not operator-selectable in the catalog. Each carries a
|
|
// reason; when one of these models joins a catalog lineup, delete the
|
|
// row here — the collision test fails loudly if the rates ever disagree.
|
|
var supplementalDefaults = map[string]map[string]Entry{
|
|
"openai": {
|
|
// GPT-5 (2025) family — kept for gateway requests using the
|
|
// unsuffixed ids; the dashboard offers only the 5.x lineup.
|
|
"gpt-5": {InputPer1k: 0.00125, OutputPer1k: 0.01, CachedInputPer1k: 0.000125},
|
|
"gpt-5-mini": {InputPer1k: 0.00025, OutputPer1k: 0.002, CachedInputPer1k: 0.000025},
|
|
"gpt-5-nano": {InputPer1k: 0.00005, OutputPer1k: 0.0004, CachedInputPer1k: 0.000005},
|
|
},
|
|
"anthropic": {
|
|
// claude-opus-5 is not yet in the catalog lineup but gateway /
|
|
// grandfathered traffic uses it; priced so it isn't skipped.
|
|
"claude-opus-5": {InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625},
|
|
// "kimi-k3[1m]" is the 1M-context alias some Claude Code guides
|
|
// configure against Moonshot's Anthropic-compatible endpoint;
|
|
// priced identically to kimi-k3 so those requests aren't skipped.
|
|
"kimi-k3[1m]": {InputPer1k: 0.003, OutputPer1k: 0.015, CacheReadPer1k: 0.0003},
|
|
},
|
|
"bedrock": {
|
|
"anthropic.claude-opus-5": {InputPer1k: 0.005, OutputPer1k: 0.025, CacheReadPer1k: 0.0005, CacheCreationPer1k: 0.00625},
|
|
},
|
|
}
|
|
|
|
var (
|
|
compiledOnce sync.Once
|
|
compiledTable map[string]map[string]Entry
|
|
// mergedTable holds the current live table when a pricing defaults
|
|
// file is loaded: the file merged entry-whole over the compiled-in
|
|
// base. Nil while no file is loaded (or after the file is removed),
|
|
// in which case the compiled-in table serves. Swapped atomically by
|
|
// the file loader/reloader; readers never block.
|
|
mergedTable atomic.Pointer[map[string]map[string]Entry]
|
|
)
|
|
|
|
// DefaultTable returns the current default pricing table keyed
|
|
// surface -> model -> Entry: the management-side defaults file (see
|
|
// LoadFile / StartReloader) when one is loaded, merged over the
|
|
// compiled-in catalog table, which alone serves as the fallback when no
|
|
// file exists. The snapshot may change between calls as the file is
|
|
// re-read — consumers (the synthesizer on every reconcile, the catalog
|
|
// endpoint on every request) pick up fresh rates automatically. Callers
|
|
// must not mutate the returned maps.
|
|
func DefaultTable() map[string]map[string]Entry {
|
|
if t := mergedTable.Load(); t != nil {
|
|
return *t
|
|
}
|
|
return compiledBase()
|
|
}
|
|
|
|
// compiledBase returns the compiled-in table (catalog + supplementals),
|
|
// built once.
|
|
func compiledBase() map[string]map[string]Entry {
|
|
compiledOnce.Do(func() {
|
|
compiledTable = buildDefaultTable()
|
|
})
|
|
return compiledTable
|
|
}
|
|
|
|
func buildDefaultTable() map[string]map[string]Entry {
|
|
out := make(map[string]map[string]Entry)
|
|
for _, p := range catalog.All() {
|
|
for _, surface := range p.PricingSurfaces {
|
|
inner, ok := out[surface]
|
|
if !ok {
|
|
inner = make(map[string]Entry)
|
|
out[surface] = inner
|
|
}
|
|
for _, m := range p.Models {
|
|
// First writer wins; providers contributing the same
|
|
// (surface, model) must agree on rates — enforced by
|
|
// TestDefaultTable_NoConflictingContributions.
|
|
if _, dup := inner[m.ID]; dup {
|
|
continue
|
|
}
|
|
inner[m.ID] = entryFromCatalogModel(m)
|
|
}
|
|
}
|
|
}
|
|
for surface, models := range supplementalDefaults {
|
|
inner, ok := out[surface]
|
|
if !ok {
|
|
inner = make(map[string]Entry)
|
|
out[surface] = inner
|
|
}
|
|
for id, e := range models {
|
|
if _, dup := inner[id]; dup {
|
|
continue
|
|
}
|
|
inner[id] = e
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func entryFromCatalogModel(m catalog.Model) Entry {
|
|
return Entry{
|
|
InputPer1k: m.InputPer1k,
|
|
OutputPer1k: m.OutputPer1k,
|
|
CachedInputPer1k: m.CachedInputPer1k,
|
|
CacheReadPer1k: m.CacheReadPer1k,
|
|
CacheCreationPer1k: m.CacheCreationPer1k,
|
|
}
|
|
}
|
|
|
|
// LookupDefault returns the default entry for model on the first of the
|
|
// given surfaces that prices it. Used by the synthesizer to seed a
|
|
// per-provider entry with default cache rates before overlaying the
|
|
// operator's stored prices.
|
|
func LookupDefault(surfaces []string, model string) (Entry, bool) {
|
|
table := DefaultTable()
|
|
for _, s := range surfaces {
|
|
if e, ok := table[s][model]; ok {
|
|
return e, true
|
|
}
|
|
}
|
|
return Entry{}, false
|
|
}
|