| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202 |
- package ocm
- import (
- "encoding/json"
- "fmt"
- "math"
- "os"
- "regexp"
- "strings"
- "sync"
- "time"
- "github.com/sagernet/sing-box/log"
- E "github.com/sagernet/sing/common/exceptions"
- )
- type UsageStats struct {
- RequestCount int `json:"request_count"`
- InputTokens int64 `json:"input_tokens"`
- OutputTokens int64 `json:"output_tokens"`
- CachedTokens int64 `json:"cached_tokens"`
- }
- func (u *UsageStats) UnmarshalJSON(data []byte) error {
- type Alias UsageStats
- aux := &struct {
- *Alias
- PromptTokens int64 `json:"prompt_tokens"`
- CompletionTokens int64 `json:"completion_tokens"`
- }{
- Alias: (*Alias)(u),
- }
- err := json.Unmarshal(data, aux)
- if err != nil {
- return err
- }
- if u.InputTokens == 0 && aux.PromptTokens > 0 {
- u.InputTokens = aux.PromptTokens
- }
- if u.OutputTokens == 0 && aux.CompletionTokens > 0 {
- u.OutputTokens = aux.CompletionTokens
- }
- return nil
- }
- type CostCombination struct {
- Model string `json:"model"`
- ServiceTier string `json:"service_tier,omitempty"`
- ContextWindow int `json:"context_window"`
- WeekStartUnix int64 `json:"week_start_unix,omitempty"`
- Total UsageStats `json:"total"`
- ByUser map[string]UsageStats `json:"by_user"`
- }
- type AggregatedUsage struct {
- LastUpdated time.Time `json:"last_updated"`
- Combinations []CostCombination `json:"combinations"`
- mutex sync.Mutex
- filePath string
- logger log.ContextLogger
- lastSaveTime time.Time
- pendingSave bool
- saveTimer *time.Timer
- saveMutex sync.Mutex
- }
- type UsageStatsJSON struct {
- RequestCount int `json:"request_count"`
- InputTokens int64 `json:"input_tokens"`
- OutputTokens int64 `json:"output_tokens"`
- CachedTokens int64 `json:"cached_tokens"`
- CostUSD float64 `json:"cost_usd"`
- }
- type CostCombinationJSON struct {
- Model string `json:"model"`
- ServiceTier string `json:"service_tier,omitempty"`
- ContextWindow int `json:"context_window"`
- WeekStartUnix int64 `json:"week_start_unix,omitempty"`
- Total UsageStatsJSON `json:"total"`
- ByUser map[string]UsageStatsJSON `json:"by_user"`
- }
- type CostsSummaryJSON struct {
- TotalUSD float64 `json:"total_usd"`
- ByUser map[string]float64 `json:"by_user"`
- ByWeek map[string]float64 `json:"by_week,omitempty"`
- ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"`
- }
- type AggregatedUsageJSON struct {
- LastUpdated time.Time `json:"last_updated"`
- Costs CostsSummaryJSON `json:"costs"`
- Combinations []CostCombinationJSON `json:"combinations"`
- }
- type WeeklyCycleHint struct {
- WindowMinutes int64
- ResetAt time.Time
- }
- type ModelPricing struct {
- InputPrice float64
- OutputPrice float64
- CachedInputPrice float64
- }
- type modelFamily struct {
- pattern *regexp.Regexp
- pricing ModelPricing
- premiumPricing *ModelPricing
- }
- const (
- serviceTierAuto = "auto"
- serviceTierDefault = "default"
- serviceTierFlex = "flex"
- serviceTierPriority = "priority"
- serviceTierScale = "scale"
- )
- const (
- contextWindowStandard = 272000
- contextWindowPremium = 1050000
- premiumContextThreshold = 272000
- )
- var (
- gpt52Pricing = ModelPricing{
- InputPrice: 1.75,
- OutputPrice: 14.0,
- CachedInputPrice: 0.175,
- }
- gpt5Pricing = ModelPricing{
- InputPrice: 1.25,
- OutputPrice: 10.0,
- CachedInputPrice: 0.125,
- }
- gpt5MiniPricing = ModelPricing{
- InputPrice: 0.25,
- OutputPrice: 2.0,
- CachedInputPrice: 0.025,
- }
- gpt5NanoPricing = ModelPricing{
- InputPrice: 0.05,
- OutputPrice: 0.4,
- CachedInputPrice: 0.005,
- }
- gpt52CodexPricing = ModelPricing{
- InputPrice: 1.75,
- OutputPrice: 14.0,
- CachedInputPrice: 0.175,
- }
- gpt51CodexPricing = ModelPricing{
- InputPrice: 1.25,
- OutputPrice: 10.0,
- CachedInputPrice: 0.125,
- }
- gpt51CodexMiniPricing = ModelPricing{
- InputPrice: 0.25,
- OutputPrice: 2.0,
- CachedInputPrice: 0.025,
- }
- gpt54StandardPricing = ModelPricing{
- InputPrice: 2.5,
- OutputPrice: 15.0,
- CachedInputPrice: 0.25,
- }
- gpt54PremiumPricing = ModelPricing{
- InputPrice: 5.0,
- OutputPrice: 22.5,
- CachedInputPrice: 0.5,
- }
- gpt54ProPricing = ModelPricing{
- InputPrice: 30.0,
- OutputPrice: 180.0,
- CachedInputPrice: 30.0,
- }
- gpt54ProPremiumPricing = ModelPricing{
- InputPrice: 60.0,
- OutputPrice: 270.0,
- CachedInputPrice: 60.0,
- }
- gpt52ProPricing = ModelPricing{
- InputPrice: 21.0,
- OutputPrice: 168.0,
- CachedInputPrice: 21.0,
- }
- gpt5ProPricing = ModelPricing{
- InputPrice: 15.0,
- OutputPrice: 120.0,
- CachedInputPrice: 15.0,
- }
- gpt54FlexPricing = ModelPricing{
- InputPrice: 1.25,
- OutputPrice: 7.5,
- CachedInputPrice: 0.125,
- }
- gpt54PremiumFlexPricing = ModelPricing{
- InputPrice: 2.5,
- OutputPrice: 11.25,
- CachedInputPrice: 0.25,
- }
- gpt54ProFlexPricing = ModelPricing{
- InputPrice: 15.0,
- OutputPrice: 90.0,
- CachedInputPrice: 15.0,
- }
- gpt54ProPremiumFlexPricing = ModelPricing{
- InputPrice: 30.0,
- OutputPrice: 135.0,
- CachedInputPrice: 30.0,
- }
- gpt52FlexPricing = ModelPricing{
- InputPrice: 0.875,
- OutputPrice: 7.0,
- CachedInputPrice: 0.0875,
- }
- gpt5FlexPricing = ModelPricing{
- InputPrice: 0.625,
- OutputPrice: 5.0,
- CachedInputPrice: 0.0625,
- }
- gpt5MiniFlexPricing = ModelPricing{
- InputPrice: 0.125,
- OutputPrice: 1.0,
- CachedInputPrice: 0.0125,
- }
- gpt5NanoFlexPricing = ModelPricing{
- InputPrice: 0.025,
- OutputPrice: 0.2,
- CachedInputPrice: 0.0025,
- }
- gpt54PriorityPricing = ModelPricing{
- InputPrice: 5.0,
- OutputPrice: 30.0,
- CachedInputPrice: 0.5,
- }
- gpt54PremiumPriorityPricing = ModelPricing{
- InputPrice: 10.0,
- OutputPrice: 45.0,
- CachedInputPrice: 1.0,
- }
- gpt52PriorityPricing = ModelPricing{
- InputPrice: 3.5,
- OutputPrice: 28.0,
- CachedInputPrice: 0.35,
- }
- gpt5PriorityPricing = ModelPricing{
- InputPrice: 2.5,
- OutputPrice: 20.0,
- CachedInputPrice: 0.25,
- }
- gpt5MiniPriorityPricing = ModelPricing{
- InputPrice: 0.45,
- OutputPrice: 3.6,
- CachedInputPrice: 0.045,
- }
- gpt52CodexPriorityPricing = ModelPricing{
- InputPrice: 3.5,
- OutputPrice: 28.0,
- CachedInputPrice: 0.35,
- }
- gpt51CodexPriorityPricing = ModelPricing{
- InputPrice: 2.5,
- OutputPrice: 20.0,
- CachedInputPrice: 0.25,
- }
- gpt4oPricing = ModelPricing{
- InputPrice: 2.5,
- OutputPrice: 10.0,
- CachedInputPrice: 1.25,
- }
- gpt4oMiniPricing = ModelPricing{
- InputPrice: 0.15,
- OutputPrice: 0.6,
- CachedInputPrice: 0.075,
- }
- gpt4oAudioPricing = ModelPricing{
- InputPrice: 2.5,
- OutputPrice: 10.0,
- CachedInputPrice: 2.5,
- }
- gpt4oMiniAudioPricing = ModelPricing{
- InputPrice: 0.15,
- OutputPrice: 0.6,
- CachedInputPrice: 0.15,
- }
- gptAudioMiniPricing = ModelPricing{
- InputPrice: 0.6,
- OutputPrice: 2.4,
- CachedInputPrice: 0.6,
- }
- o1Pricing = ModelPricing{
- InputPrice: 15.0,
- OutputPrice: 60.0,
- CachedInputPrice: 7.5,
- }
- o1ProPricing = ModelPricing{
- InputPrice: 150.0,
- OutputPrice: 600.0,
- CachedInputPrice: 150.0,
- }
- o1MiniPricing = ModelPricing{
- InputPrice: 1.1,
- OutputPrice: 4.4,
- CachedInputPrice: 0.55,
- }
- o3MiniPricing = ModelPricing{
- InputPrice: 1.1,
- OutputPrice: 4.4,
- CachedInputPrice: 0.55,
- }
- o3Pricing = ModelPricing{
- InputPrice: 2.0,
- OutputPrice: 8.0,
- CachedInputPrice: 0.5,
- }
- o3ProPricing = ModelPricing{
- InputPrice: 20.0,
- OutputPrice: 80.0,
- CachedInputPrice: 20.0,
- }
- o3DeepResearchPricing = ModelPricing{
- InputPrice: 10.0,
- OutputPrice: 40.0,
- CachedInputPrice: 2.5,
- }
- o4MiniPricing = ModelPricing{
- InputPrice: 1.1,
- OutputPrice: 4.4,
- CachedInputPrice: 0.275,
- }
- o4MiniDeepResearchPricing = ModelPricing{
- InputPrice: 2.0,
- OutputPrice: 8.0,
- CachedInputPrice: 0.5,
- }
- o3FlexPricing = ModelPricing{
- InputPrice: 1.0,
- OutputPrice: 4.0,
- CachedInputPrice: 0.25,
- }
- o4MiniFlexPricing = ModelPricing{
- InputPrice: 0.55,
- OutputPrice: 2.2,
- CachedInputPrice: 0.138,
- }
- o3PriorityPricing = ModelPricing{
- InputPrice: 3.5,
- OutputPrice: 14.0,
- CachedInputPrice: 0.875,
- }
- o4MiniPriorityPricing = ModelPricing{
- InputPrice: 2.0,
- OutputPrice: 8.0,
- CachedInputPrice: 0.5,
- }
- gpt41Pricing = ModelPricing{
- InputPrice: 2.0,
- OutputPrice: 8.0,
- CachedInputPrice: 0.5,
- }
- gpt41MiniPricing = ModelPricing{
- InputPrice: 0.4,
- OutputPrice: 1.6,
- CachedInputPrice: 0.1,
- }
- gpt41NanoPricing = ModelPricing{
- InputPrice: 0.1,
- OutputPrice: 0.4,
- CachedInputPrice: 0.025,
- }
- gpt41PriorityPricing = ModelPricing{
- InputPrice: 3.5,
- OutputPrice: 14.0,
- CachedInputPrice: 0.875,
- }
- gpt41MiniPriorityPricing = ModelPricing{
- InputPrice: 0.7,
- OutputPrice: 2.8,
- CachedInputPrice: 0.175,
- }
- gpt41NanoPriorityPricing = ModelPricing{
- InputPrice: 0.2,
- OutputPrice: 0.8,
- CachedInputPrice: 0.05,
- }
- gpt4oPriorityPricing = ModelPricing{
- InputPrice: 4.25,
- OutputPrice: 17.0,
- CachedInputPrice: 2.125,
- }
- gpt4oMiniPriorityPricing = ModelPricing{
- InputPrice: 0.25,
- OutputPrice: 1.0,
- CachedInputPrice: 0.125,
- }
- standardModelFamilies = []modelFamily{
- {
- pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`),
- pricing: gpt54ProPricing,
- premiumPricing: &gpt54ProPremiumPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`),
- pricing: gpt54StandardPricing,
- premiumPricing: &gpt54PremiumPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`),
- pricing: gpt52CodexPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`),
- pricing: gpt52CodexPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`),
- pricing: gpt51CodexPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.1-codex-mini(?:$|-)`),
- pricing: gpt51CodexMiniPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`),
- pricing: gpt51CodexPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5-codex-mini(?:$|-)`),
- pricing: gpt51CodexMiniPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`),
- pricing: gpt51CodexPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.2-chat-latest$`),
- pricing: gpt52Pricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.1-chat-latest$`),
- pricing: gpt5Pricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5-chat-latest$`),
- pricing: gpt5Pricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.2-pro(?:$|-)`),
- pricing: gpt52ProPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5-pro(?:$|-)`),
- pricing: gpt5ProPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
- pricing: gpt5MiniPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`),
- pricing: gpt5NanoPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
- pricing: gpt52Pricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
- pricing: gpt5Pricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
- pricing: gpt5Pricing,
- },
- {
- pattern: regexp.MustCompile(`^o4-mini-deep-research(?:$|-)`),
- pricing: o4MiniDeepResearchPricing,
- },
- {
- pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
- pricing: o4MiniPricing,
- },
- {
- pattern: regexp.MustCompile(`^o3-pro(?:$|-)`),
- pricing: o3ProPricing,
- },
- {
- pattern: regexp.MustCompile(`^o3-deep-research(?:$|-)`),
- pricing: o3DeepResearchPricing,
- },
- {
- pattern: regexp.MustCompile(`^o3-mini(?:$|-)`),
- pricing: o3MiniPricing,
- },
- {
- pattern: regexp.MustCompile(`^o3(?:$|-)`),
- pricing: o3Pricing,
- },
- {
- pattern: regexp.MustCompile(`^o1-pro(?:$|-)`),
- pricing: o1ProPricing,
- },
- {
- pattern: regexp.MustCompile(`^o1-mini(?:$|-)`),
- pricing: o1MiniPricing,
- },
- {
- pattern: regexp.MustCompile(`^o1(?:$|-)`),
- pricing: o1Pricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-4o-mini-audio(?:$|-)`),
- pricing: gpt4oMiniAudioPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-audio-mini(?:$|-)`),
- pricing: gptAudioMiniPricing,
- },
- {
- pattern: regexp.MustCompile(`^(?:gpt-4o-audio|gpt-audio)(?:$|-)`),
- pricing: gpt4oAudioPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`),
- pricing: gpt41NanoPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`),
- pricing: gpt41MiniPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`),
- pricing: gpt41Pricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`),
- pricing: gpt4oMiniPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`),
- pricing: gpt4oPricing,
- },
- {
- pattern: regexp.MustCompile(`^chatgpt-4o(?:$|-)`),
- pricing: gpt4oPricing,
- },
- }
- flexModelFamilies = []modelFamily{
- {
- pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`),
- pricing: gpt54ProFlexPricing,
- premiumPricing: &gpt54ProPremiumFlexPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`),
- pricing: gpt54FlexPricing,
- premiumPricing: &gpt54PremiumFlexPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
- pricing: gpt5MiniFlexPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`),
- pricing: gpt5NanoFlexPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
- pricing: gpt52FlexPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
- pricing: gpt5FlexPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
- pricing: gpt5FlexPricing,
- },
- {
- pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
- pricing: o4MiniFlexPricing,
- },
- {
- pattern: regexp.MustCompile(`^o3(?:$|-)`),
- pricing: o3FlexPricing,
- },
- }
- priorityModelFamilies = []modelFamily{
- {
- pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`),
- pricing: gpt54PriorityPricing,
- premiumPricing: &gpt54PremiumPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`),
- pricing: gpt52CodexPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`),
- pricing: gpt52CodexPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`),
- pricing: gpt51CodexPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`),
- pricing: gpt51CodexPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5-codex-mini(?:$|-)`),
- pricing: gpt5MiniPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`),
- pricing: gpt51CodexPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
- pricing: gpt5MiniPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
- pricing: gpt52PriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
- pricing: gpt5PriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
- pricing: gpt5PriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
- pricing: o4MiniPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^o3(?:$|-)`),
- pricing: o3PriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`),
- pricing: gpt41NanoPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`),
- pricing: gpt41MiniPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`),
- pricing: gpt41PriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`),
- pricing: gpt4oMiniPriorityPricing,
- },
- {
- pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`),
- pricing: gpt4oPriorityPricing,
- },
- }
- )
- func modelFamiliesForTier(serviceTier string) []modelFamily {
- switch serviceTier {
- case serviceTierFlex:
- return flexModelFamilies
- case serviceTierPriority:
- return priorityModelFamilies
- default:
- return standardModelFamilies
- }
- }
- func findPricingInFamilies(model string, contextWindow int, modelFamilies []modelFamily) (ModelPricing, bool) {
- isPremium := contextWindow >= contextWindowPremium
- for _, family := range modelFamilies {
- if family.pattern.MatchString(model) {
- if isPremium && family.premiumPricing != nil {
- return *family.premiumPricing, true
- }
- return family.pricing, true
- }
- }
- return ModelPricing{}, false
- }
- func hasPremiumPricingInFamilies(model string, modelFamilies []modelFamily) bool {
- for _, family := range modelFamilies {
- if family.pattern.MatchString(model) {
- return family.premiumPricing != nil
- }
- }
- return false
- }
- func normalizeServiceTier(serviceTier string) string {
- switch strings.ToLower(strings.TrimSpace(serviceTier)) {
- case "", serviceTierAuto, serviceTierDefault:
- return serviceTierDefault
- case serviceTierFlex:
- return serviceTierFlex
- case serviceTierPriority:
- return serviceTierPriority
- case serviceTierScale:
- // Scale-tier requests are prepaid differently and not listed in this usage file.
- return serviceTierDefault
- default:
- return serviceTierDefault
- }
- }
- func getPricing(model string, serviceTier string, contextWindow int) ModelPricing {
- normalizedServiceTier := normalizeServiceTier(serviceTier)
- families := modelFamiliesForTier(normalizedServiceTier)
- if pricing, found := findPricingInFamilies(model, contextWindow, families); found {
- return pricing
- }
- normalizedModel := normalizeGPT5Model(model)
- if normalizedModel != model {
- if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, families); found {
- return pricing
- }
- }
- if normalizedServiceTier != serviceTierDefault {
- if pricing, found := findPricingInFamilies(model, contextWindow, standardModelFamilies); found {
- return pricing
- }
- if normalizedModel != model {
- if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, standardModelFamilies); found {
- return pricing
- }
- }
- }
- return gpt4oPricing
- }
- func detectContextWindow(model string, serviceTier string, inputTokens int64) int {
- if inputTokens <= premiumContextThreshold {
- return contextWindowStandard
- }
- normalizedServiceTier := normalizeServiceTier(serviceTier)
- families := modelFamiliesForTier(normalizedServiceTier)
- if hasPremiumPricingInFamilies(model, families) {
- return contextWindowPremium
- }
- normalizedModel := normalizeGPT5Model(model)
- if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, families) {
- return contextWindowPremium
- }
- if normalizedServiceTier != serviceTierDefault {
- if hasPremiumPricingInFamilies(model, standardModelFamilies) {
- return contextWindowPremium
- }
- if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, standardModelFamilies) {
- return contextWindowPremium
- }
- }
- return contextWindowStandard
- }
- func normalizeGPT5Model(model string) string {
- if !strings.HasPrefix(model, "gpt-5.") {
- return model
- }
- switch {
- case strings.Contains(model, "-codex-mini"):
- return "gpt-5.1-codex-mini"
- case strings.Contains(model, "-codex-max"):
- return "gpt-5.1-codex-max"
- case strings.Contains(model, "-codex"):
- return "gpt-5.3-codex"
- case strings.Contains(model, "-chat-latest"):
- return "gpt-5.2-chat-latest"
- case strings.Contains(model, "-pro"):
- return "gpt-5.4-pro"
- case strings.Contains(model, "-mini"):
- return "gpt-5-mini"
- case strings.Contains(model, "-nano"):
- return "gpt-5-nano"
- default:
- return "gpt-5.4"
- }
- }
- func calculateCost(stats UsageStats, model string, serviceTier string, contextWindow int) float64 {
- pricing := getPricing(model, serviceTier, contextWindow)
- regularInputTokens := stats.InputTokens - stats.CachedTokens
- if regularInputTokens < 0 {
- regularInputTokens = 0
- }
- cost := (float64(regularInputTokens)*pricing.InputPrice +
- float64(stats.OutputTokens)*pricing.OutputPrice +
- float64(stats.CachedTokens)*pricing.CachedInputPrice) / 1_000_000
- return math.Round(cost*100) / 100
- }
- func roundCost(cost float64) float64 {
- return math.Round(cost*100) / 100
- }
- func normalizeCombinations(combinations []CostCombination) {
- for index := range combinations {
- combinations[index].ServiceTier = normalizeServiceTier(combinations[index].ServiceTier)
- if combinations[index].ContextWindow <= 0 {
- combinations[index].ContextWindow = contextWindowStandard
- }
- if combinations[index].ByUser == nil {
- combinations[index].ByUser = make(map[string]UsageStats)
- }
- }
- }
- func addUsageToCombinations(combinations *[]CostCombination, model string, serviceTier string, contextWindow int, weekStartUnix int64, user string, inputTokens, outputTokens, cachedTokens int64) {
- var matchedCombination *CostCombination
- for index := range *combinations {
- combination := &(*combinations)[index]
- combinationServiceTier := normalizeServiceTier(combination.ServiceTier)
- if combination.ServiceTier != combinationServiceTier {
- combination.ServiceTier = combinationServiceTier
- }
- if combination.Model == model && combinationServiceTier == serviceTier && combination.ContextWindow == contextWindow && combination.WeekStartUnix == weekStartUnix {
- matchedCombination = combination
- break
- }
- }
- if matchedCombination == nil {
- newCombination := CostCombination{
- Model: model,
- ServiceTier: serviceTier,
- ContextWindow: contextWindow,
- WeekStartUnix: weekStartUnix,
- Total: UsageStats{},
- ByUser: make(map[string]UsageStats),
- }
- *combinations = append(*combinations, newCombination)
- matchedCombination = &(*combinations)[len(*combinations)-1]
- }
- matchedCombination.Total.RequestCount++
- matchedCombination.Total.InputTokens += inputTokens
- matchedCombination.Total.OutputTokens += outputTokens
- matchedCombination.Total.CachedTokens += cachedTokens
- if user != "" {
- userStats := matchedCombination.ByUser[user]
- userStats.RequestCount++
- userStats.InputTokens += inputTokens
- userStats.OutputTokens += outputTokens
- userStats.CachedTokens += cachedTokens
- matchedCombination.ByUser[user] = userStats
- }
- }
- func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map[string]float64) ([]CostCombinationJSON, float64) {
- result := make([]CostCombinationJSON, len(combinations))
- var totalCost float64
- for index, combination := range combinations {
- combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow)
- totalCost += combinationTotalCost
- combinationJSON := CostCombinationJSON{
- Model: combination.Model,
- ServiceTier: combination.ServiceTier,
- ContextWindow: combination.ContextWindow,
- WeekStartUnix: combination.WeekStartUnix,
- Total: UsageStatsJSON{
- RequestCount: combination.Total.RequestCount,
- InputTokens: combination.Total.InputTokens,
- OutputTokens: combination.Total.OutputTokens,
- CachedTokens: combination.Total.CachedTokens,
- CostUSD: combinationTotalCost,
- },
- ByUser: make(map[string]UsageStatsJSON),
- }
- for user, userStats := range combination.ByUser {
- userCost := calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow)
- if aggregateUserCosts != nil {
- aggregateUserCosts[user] += userCost
- }
- combinationJSON.ByUser[user] = UsageStatsJSON{
- RequestCount: userStats.RequestCount,
- InputTokens: userStats.InputTokens,
- OutputTokens: userStats.OutputTokens,
- CachedTokens: userStats.CachedTokens,
- CostUSD: userCost,
- }
- }
- result[index] = combinationJSON
- }
- return result, roundCost(totalCost)
- }
- func formatUTCOffsetLabel(timestamp time.Time) string {
- _, offsetSeconds := timestamp.Zone()
- sign := "+"
- if offsetSeconds < 0 {
- sign = "-"
- offsetSeconds = -offsetSeconds
- }
- offsetHours := offsetSeconds / 3600
- offsetMinutes := (offsetSeconds % 3600) / 60
- if offsetMinutes == 0 {
- return fmt.Sprintf("UTC%s%d", sign, offsetHours)
- }
- return fmt.Sprintf("UTC%s%d:%02d", sign, offsetHours, offsetMinutes)
- }
- func formatWeekStartKey(cycleStartAt time.Time) string {
- localCycleStart := cycleStartAt.In(time.Local)
- return fmt.Sprintf("%s %s", localCycleStart.Format("2006-01-02 15:04:05"), formatUTCOffsetLabel(localCycleStart))
- }
- func buildByWeekCost(combinations []CostCombination) map[string]float64 {
- byWeek := make(map[string]float64)
- for _, combination := range combinations {
- if combination.WeekStartUnix <= 0 {
- continue
- }
- weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC()
- weekKey := formatWeekStartKey(weekStartAt)
- byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow)
- }
- for weekKey, weekCost := range byWeek {
- byWeek[weekKey] = roundCost(weekCost)
- }
- return byWeek
- }
- func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 {
- byUserAndWeek := make(map[string]map[string]float64)
- for _, combination := range combinations {
- if combination.WeekStartUnix <= 0 {
- continue
- }
- weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC()
- weekKey := formatWeekStartKey(weekStartAt)
- for user, userStats := range combination.ByUser {
- userWeeks, exists := byUserAndWeek[user]
- if !exists {
- userWeeks = make(map[string]float64)
- byUserAndWeek[user] = userWeeks
- }
- userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow)
- }
- }
- for _, weekCosts := range byUserAndWeek {
- for weekKey, cost := range weekCosts {
- weekCosts[weekKey] = roundCost(cost)
- }
- }
- return byUserAndWeek
- }
- func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 {
- if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() {
- return 0
- }
- windowDuration := time.Duration(cycleHint.WindowMinutes) * time.Minute
- return cycleHint.ResetAt.UTC().Add(-windowDuration).Unix()
- }
- func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
- u.mutex.Lock()
- defer u.mutex.Unlock()
- result := &AggregatedUsageJSON{
- LastUpdated: u.LastUpdated,
- Costs: CostsSummaryJSON{
- TotalUSD: 0,
- ByUser: make(map[string]float64),
- ByWeek: make(map[string]float64),
- },
- }
- globalCombinationsJSON, totalCost := buildCombinationJSON(u.Combinations, result.Costs.ByUser)
- result.Combinations = globalCombinationsJSON
- result.Costs.TotalUSD = totalCost
- result.Costs.ByWeek = buildByWeekCost(u.Combinations)
- if len(result.Costs.ByWeek) == 0 {
- result.Costs.ByWeek = nil
- }
- result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations)
- if len(result.Costs.ByUserAndWeek) == 0 {
- result.Costs.ByUserAndWeek = nil
- }
- for user, cost := range result.Costs.ByUser {
- result.Costs.ByUser[user] = roundCost(cost)
- }
- return result
- }
- func (u *AggregatedUsage) Load() error {
- u.mutex.Lock()
- defer u.mutex.Unlock()
- u.LastUpdated = time.Time{}
- u.Combinations = nil
- data, err := os.ReadFile(u.filePath)
- if err != nil {
- if os.IsNotExist(err) {
- return nil
- }
- return err
- }
- var temp struct {
- LastUpdated time.Time `json:"last_updated"`
- Combinations []CostCombination `json:"combinations"`
- }
- err = json.Unmarshal(data, &temp)
- if err != nil {
- return err
- }
- u.LastUpdated = temp.LastUpdated
- u.Combinations = temp.Combinations
- normalizeCombinations(u.Combinations)
- return nil
- }
- func (u *AggregatedUsage) Save() error {
- jsonData := u.ToJSON()
- data, err := json.MarshalIndent(jsonData, "", " ")
- if err != nil {
- return err
- }
- tmpFile := u.filePath + ".tmp"
- err = os.WriteFile(tmpFile, data, 0o644)
- if err != nil {
- return err
- }
- defer os.Remove(tmpFile)
- err = os.Rename(tmpFile, u.filePath)
- if err == nil {
- u.saveMutex.Lock()
- u.lastSaveTime = time.Now()
- u.saveMutex.Unlock()
- }
- return err
- }
- func (u *AggregatedUsage) AddUsage(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string) error {
- return u.AddUsageWithCycleHint(model, contextWindow, inputTokens, outputTokens, cachedTokens, serviceTier, user, time.Now(), nil)
- }
- func (u *AggregatedUsage) AddUsageWithCycleHint(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string, observedAt time.Time, cycleHint *WeeklyCycleHint) error {
- if model == "" {
- return E.New("model cannot be empty")
- }
- if contextWindow <= 0 {
- return E.New("contextWindow must be positive")
- }
- normalizedServiceTier := normalizeServiceTier(serviceTier)
- if observedAt.IsZero() {
- observedAt = time.Now()
- }
- u.mutex.Lock()
- defer u.mutex.Unlock()
- u.LastUpdated = observedAt
- weekStartUnix := deriveWeekStartUnix(cycleHint)
- addUsageToCombinations(&u.Combinations, model, normalizedServiceTier, contextWindow, weekStartUnix, user, inputTokens, outputTokens, cachedTokens)
- go u.scheduleSave()
- return nil
- }
- func (u *AggregatedUsage) scheduleSave() {
- const saveInterval = time.Minute
- u.saveMutex.Lock()
- defer u.saveMutex.Unlock()
- timeSinceLastSave := time.Since(u.lastSaveTime)
- if timeSinceLastSave >= saveInterval {
- go u.saveAsync()
- return
- }
- if u.pendingSave {
- return
- }
- u.pendingSave = true
- remainingTime := saveInterval - timeSinceLastSave
- u.saveTimer = time.AfterFunc(remainingTime, func() {
- u.saveMutex.Lock()
- u.pendingSave = false
- u.saveMutex.Unlock()
- u.saveAsync()
- })
- }
- func (u *AggregatedUsage) saveAsync() {
- err := u.Save()
- if err != nil {
- if u.logger != nil {
- u.logger.Error("save usage statistics: ", err)
- }
- }
- }
- func (u *AggregatedUsage) cancelPendingSave() {
- u.saveMutex.Lock()
- defer u.saveMutex.Unlock()
- if u.saveTimer != nil {
- u.saveTimer.Stop()
- u.saveTimer = nil
- }
- u.pendingSave = false
- }
|