mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-14 20:58:33 +10:00
1203 lines
29 KiB
Go
1203 lines
29 KiB
Go
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"`
|
|
access sync.Mutex
|
|
filePath string
|
|
logger log.ContextLogger
|
|
lastSaveTime time.Time
|
|
pendingSave bool
|
|
saveTimer *time.Timer
|
|
saveAccess 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.access.Lock()
|
|
defer u.access.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.access.Lock()
|
|
defer u.access.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.saveAccess.Lock()
|
|
u.lastSaveTime = time.Now()
|
|
u.saveAccess.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.access.Lock()
|
|
defer u.access.Unlock()
|
|
|
|
u.LastUpdated = observedAt
|
|
weekStartUnix := deriveWeekStartUnix(cycleHint)
|
|
|
|
addUsageToCombinations(&u.Combinations, model, normalizedServiceTier, contextWindow, weekStartUnix, user, inputTokens, outputTokens, cachedTokens)
|
|
|
|
u.scheduleSave()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *AggregatedUsage) scheduleSave() {
|
|
const saveInterval = time.Minute
|
|
|
|
u.saveAccess.Lock()
|
|
defer u.saveAccess.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.saveAccess.Lock()
|
|
u.pendingSave = false
|
|
u.saveAccess.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.saveAccess.Lock()
|
|
defer u.saveAccess.Unlock()
|
|
|
|
if u.saveTimer != nil {
|
|
u.saveTimer.Stop()
|
|
u.saveTimer = nil
|
|
}
|
|
u.pendingSave = false
|
|
}
|