mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-14 20:58:33 +10:00
CCM: Fix 1M context detection - use prefix match for versioned beta strings (e.g. "context-1m-2025-08-07") and include cache tokens in the 200K threshold check per Anthropic billing docs. OCM: Add GPT-5.4 family pricing (standard/priority/flex) with extended context (>272K) premium pricing support. Add context window tracking to usage combinations, mirroring CCM's pattern. Update normalizeGPT5Model defaults to latest known models.
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"`
|
|
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
|
|
}
|