Fix OCM usage counting & Update pricing

This commit is contained in:
世界
2026-02-24 19:16:40 +08:00
parent d48236da94
commit 22cf675263
6 changed files with 748 additions and 156 deletions

View File

@@ -425,6 +425,8 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
usage.OutputTokens,
usage.CacheReadInputTokens,
usage.CacheCreationInputTokens,
usage.CacheCreation.Ephemeral5mInputTokens,
usage.CacheCreation.Ephemeral1hInputTokens,
username,
)
}
@@ -485,6 +487,8 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
accumulatedUsage.InputTokens = messageStart.Message.Usage.InputTokens
accumulatedUsage.CacheReadInputTokens = messageStart.Message.Usage.CacheReadInputTokens
accumulatedUsage.CacheCreationInputTokens = messageStart.Message.Usage.CacheCreationInputTokens
accumulatedUsage.CacheCreation.Ephemeral5mInputTokens = messageStart.Message.Usage.CacheCreation.Ephemeral5mInputTokens
accumulatedUsage.CacheCreation.Ephemeral1hInputTokens = messageStart.Message.Usage.CacheCreation.Ephemeral1hInputTokens
}
case "message_delta":
messageDelta := event.AsMessageDelta()
@@ -519,6 +523,8 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons
accumulatedUsage.OutputTokens,
accumulatedUsage.CacheReadInputTokens,
accumulatedUsage.CacheCreationInputTokens,
accumulatedUsage.CacheCreation.Ephemeral5mInputTokens,
accumulatedUsage.CacheCreation.Ephemeral1hInputTokens,
username,
)
}

View File

@@ -13,12 +13,14 @@ import (
)
type UsageStats struct {
RequestCount int `json:"request_count"`
MessagesCount int `json:"messages_count"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CacheReadInputTokens int64 `json:"cache_read_input_tokens"`
CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"`
RequestCount int `json:"request_count"`
MessagesCount int `json:"messages_count"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CacheReadInputTokens int64 `json:"cache_read_input_tokens"`
CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"`
CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"`
CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"`
}
type CostCombination struct {
@@ -41,13 +43,15 @@ type AggregatedUsage struct {
}
type UsageStatsJSON struct {
RequestCount int `json:"request_count"`
MessagesCount int `json:"messages_count"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CacheReadInputTokens int64 `json:"cache_read_input_tokens"`
CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"`
CostUSD float64 `json:"cost_usd"`
RequestCount int `json:"request_count"`
MessagesCount int `json:"messages_count"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CacheReadInputTokens int64 `json:"cache_read_input_tokens"`
CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"`
CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"`
CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"`
CostUSD float64 `json:"cost_usd"`
}
type CostCombinationJSON struct {
@@ -69,10 +73,11 @@ type AggregatedUsageJSON struct {
}
type ModelPricing struct {
InputPrice float64
OutputPrice float64
CacheReadPrice float64
CacheWritePrice float64
InputPrice float64
OutputPrice float64
CacheReadPrice float64
CacheWritePrice5Minute float64
CacheWritePrice1Hour float64
}
type modelFamily struct {
@@ -82,143 +87,205 @@ type modelFamily struct {
}
var (
opus4Pricing = ModelPricing{
InputPrice: 15.0,
OutputPrice: 75.0,
CacheReadPrice: 1.5,
CacheWritePrice: 18.75,
opus46StandardPricing = ModelPricing{
InputPrice: 5.0,
OutputPrice: 25.0,
CacheReadPrice: 0.5,
CacheWritePrice5Minute: 6.25,
CacheWritePrice1Hour: 10.0,
}
sonnet4StandardPricing = ModelPricing{
InputPrice: 3.0,
OutputPrice: 15.0,
CacheReadPrice: 0.3,
CacheWritePrice: 3.75,
}
sonnet4PremiumPricing = ModelPricing{
InputPrice: 6.0,
OutputPrice: 22.5,
CacheReadPrice: 0.6,
CacheWritePrice: 7.5,
}
haiku4Pricing = ModelPricing{
InputPrice: 1.0,
OutputPrice: 5.0,
CacheReadPrice: 0.1,
CacheWritePrice: 1.25,
}
haiku35Pricing = ModelPricing{
InputPrice: 0.8,
OutputPrice: 4.0,
CacheReadPrice: 0.08,
CacheWritePrice: 1.0,
}
sonnet35Pricing = ModelPricing{
InputPrice: 3.0,
OutputPrice: 15.0,
CacheReadPrice: 0.3,
CacheWritePrice: 3.75,
opus46PremiumPricing = ModelPricing{
InputPrice: 10.0,
OutputPrice: 37.5,
CacheReadPrice: 1.0,
CacheWritePrice5Minute: 12.5,
CacheWritePrice1Hour: 20.0,
}
opus45Pricing = ModelPricing{
InputPrice: 5.0,
OutputPrice: 25.0,
CacheReadPrice: 0.5,
CacheWritePrice: 6.25,
InputPrice: 5.0,
OutputPrice: 25.0,
CacheReadPrice: 0.5,
CacheWritePrice5Minute: 6.25,
CacheWritePrice1Hour: 10.0,
}
opus4Pricing = ModelPricing{
InputPrice: 15.0,
OutputPrice: 75.0,
CacheReadPrice: 1.5,
CacheWritePrice5Minute: 18.75,
CacheWritePrice1Hour: 30.0,
}
sonnet46StandardPricing = ModelPricing{
InputPrice: 3.0,
OutputPrice: 15.0,
CacheReadPrice: 0.3,
CacheWritePrice5Minute: 3.75,
CacheWritePrice1Hour: 6.0,
}
sonnet46PremiumPricing = ModelPricing{
InputPrice: 6.0,
OutputPrice: 22.5,
CacheReadPrice: 0.6,
CacheWritePrice5Minute: 7.5,
CacheWritePrice1Hour: 12.0,
}
sonnet45StandardPricing = ModelPricing{
InputPrice: 3.0,
OutputPrice: 15.0,
CacheReadPrice: 0.3,
CacheWritePrice: 3.75,
InputPrice: 3.0,
OutputPrice: 15.0,
CacheReadPrice: 0.3,
CacheWritePrice5Minute: 3.75,
CacheWritePrice1Hour: 6.0,
}
sonnet45PremiumPricing = ModelPricing{
InputPrice: 6.0,
OutputPrice: 22.5,
CacheReadPrice: 0.6,
CacheWritePrice: 7.5,
InputPrice: 6.0,
OutputPrice: 22.5,
CacheReadPrice: 0.6,
CacheWritePrice5Minute: 7.5,
CacheWritePrice1Hour: 12.0,
}
sonnet4StandardPricing = ModelPricing{
InputPrice: 3.0,
OutputPrice: 15.0,
CacheReadPrice: 0.3,
CacheWritePrice5Minute: 3.75,
CacheWritePrice1Hour: 6.0,
}
sonnet4PremiumPricing = ModelPricing{
InputPrice: 6.0,
OutputPrice: 22.5,
CacheReadPrice: 0.6,
CacheWritePrice5Minute: 7.5,
CacheWritePrice1Hour: 12.0,
}
sonnet37Pricing = ModelPricing{
InputPrice: 3.0,
OutputPrice: 15.0,
CacheReadPrice: 0.3,
CacheWritePrice5Minute: 3.75,
CacheWritePrice1Hour: 6.0,
}
sonnet35Pricing = ModelPricing{
InputPrice: 3.0,
OutputPrice: 15.0,
CacheReadPrice: 0.3,
CacheWritePrice5Minute: 3.75,
CacheWritePrice1Hour: 6.0,
}
haiku45Pricing = ModelPricing{
InputPrice: 1.0,
OutputPrice: 5.0,
CacheReadPrice: 0.1,
CacheWritePrice: 1.25,
InputPrice: 1.0,
OutputPrice: 5.0,
CacheReadPrice: 0.1,
CacheWritePrice5Minute: 1.25,
CacheWritePrice1Hour: 2.0,
}
haiku4Pricing = ModelPricing{
InputPrice: 1.0,
OutputPrice: 5.0,
CacheReadPrice: 0.1,
CacheWritePrice5Minute: 1.25,
CacheWritePrice1Hour: 2.0,
}
haiku35Pricing = ModelPricing{
InputPrice: 0.8,
OutputPrice: 4.0,
CacheReadPrice: 0.08,
CacheWritePrice5Minute: 1.0,
CacheWritePrice1Hour: 1.6,
}
haiku3Pricing = ModelPricing{
InputPrice: 0.25,
OutputPrice: 1.25,
CacheReadPrice: 0.03,
CacheWritePrice: 0.3,
InputPrice: 0.25,
OutputPrice: 1.25,
CacheReadPrice: 0.03,
CacheWritePrice5Minute: 0.3,
CacheWritePrice1Hour: 0.5,
}
opus3Pricing = ModelPricing{
InputPrice: 15.0,
OutputPrice: 75.0,
CacheReadPrice: 1.5,
CacheWritePrice: 18.75,
InputPrice: 15.0,
OutputPrice: 75.0,
CacheReadPrice: 1.5,
CacheWritePrice5Minute: 18.75,
CacheWritePrice1Hour: 30.0,
}
modelFamilies = []modelFamily{
{
pattern: regexp.MustCompile(`^claude-opus-4-5-`),
pattern: regexp.MustCompile(`^claude-opus-4-6(?:-|$)`),
standardPricing: opus46StandardPricing,
premiumPricing: &opus46PremiumPricing,
},
{
pattern: regexp.MustCompile(`^claude-opus-4-5(?:-|$)`),
standardPricing: opus45Pricing,
premiumPricing: nil,
},
{
pattern: regexp.MustCompile(`^claude-(?:opus-4-|4-opus-|opus-4-1-)`),
pattern: regexp.MustCompile(`^claude-(?:opus-4(?:-|$)|4-opus-)`),
standardPricing: opus4Pricing,
premiumPricing: nil,
},
{
pattern: regexp.MustCompile(`^claude-(?:opus-3-|3-opus-)`),
pattern: regexp.MustCompile(`^claude-(?:opus-3(?:-|$)|3-opus-)`),
standardPricing: opus3Pricing,
premiumPricing: nil,
},
{
pattern: regexp.MustCompile(`^claude-(?:sonnet-4-5-|4-5-sonnet-)`),
pattern: regexp.MustCompile(`^claude-(?:sonnet-4-6(?:-|$)|4-6-sonnet-)`),
standardPricing: sonnet46StandardPricing,
premiumPricing: &sonnet46PremiumPricing,
},
{
pattern: regexp.MustCompile(`^claude-(?:sonnet-4-5(?:-|$)|4-5-sonnet-)`),
standardPricing: sonnet45StandardPricing,
premiumPricing: &sonnet45PremiumPricing,
},
{
pattern: regexp.MustCompile(`^claude-3-7-sonnet-`),
pattern: regexp.MustCompile(`^claude-(?:sonnet-4(?:-|$)|4-sonnet-)`),
standardPricing: sonnet4StandardPricing,
premiumPricing: &sonnet4PremiumPricing,
},
{
pattern: regexp.MustCompile(`^claude-(?:sonnet-4-|4-sonnet-)`),
standardPricing: sonnet4StandardPricing,
premiumPricing: &sonnet4PremiumPricing,
pattern: regexp.MustCompile(`^claude-3-7-sonnet(?:-|$)`),
standardPricing: sonnet37Pricing,
premiumPricing: nil,
},
{
pattern: regexp.MustCompile(`^claude-3-5-sonnet-`),
pattern: regexp.MustCompile(`^claude-3-5-sonnet(?:-|$)`),
standardPricing: sonnet35Pricing,
premiumPricing: nil,
},
{
pattern: regexp.MustCompile(`^claude-(?:haiku-4-5-|4-5-haiku-)`),
pattern: regexp.MustCompile(`^claude-(?:haiku-4-5(?:-|$)|4-5-haiku-)`),
standardPricing: haiku45Pricing,
premiumPricing: nil,
},
{
pattern: regexp.MustCompile(`^claude-haiku-4-`),
pattern: regexp.MustCompile(`^claude-haiku-4(?:-|$)`),
standardPricing: haiku4Pricing,
premiumPricing: nil,
},
{
pattern: regexp.MustCompile(`^claude-3-5-haiku-`),
pattern: regexp.MustCompile(`^claude-3-5-haiku(?:-|$)`),
standardPricing: haiku35Pricing,
premiumPricing: nil,
},
{
pattern: regexp.MustCompile(`^claude-3-haiku-`),
pattern: regexp.MustCompile(`^claude-3-haiku(?:-|$)`),
standardPricing: haiku3Pricing,
premiumPricing: nil,
},
@@ -243,10 +310,20 @@ func getPricing(model string, contextWindow int) ModelPricing {
func calculateCost(stats UsageStats, model string, contextWindow int) float64 {
pricing := getPricing(model, contextWindow)
cacheCreationCost := 0.0
if stats.CacheCreation5MinuteInputTokens > 0 || stats.CacheCreation1HourInputTokens > 0 {
cacheCreationCost =
float64(stats.CacheCreation5MinuteInputTokens)*pricing.CacheWritePrice5Minute +
float64(stats.CacheCreation1HourInputTokens)*pricing.CacheWritePrice1Hour
} else {
// Backward compatibility for usage files generated before TTL split tracking.
cacheCreationCost = float64(stats.CacheCreationInputTokens) * pricing.CacheWritePrice5Minute
}
cost := (float64(stats.InputTokens)*pricing.InputPrice +
float64(stats.OutputTokens)*pricing.OutputPrice +
float64(stats.CacheReadInputTokens)*pricing.CacheReadPrice +
float64(stats.CacheCreationInputTokens)*pricing.CacheWritePrice) / 1_000_000
cacheCreationCost) / 1_000_000
return math.Round(cost*100) / 100
}
@@ -273,13 +350,15 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
Model: combo.Model,
ContextWindow: combo.ContextWindow,
Total: UsageStatsJSON{
RequestCount: combo.Total.RequestCount,
MessagesCount: combo.Total.MessagesCount,
InputTokens: combo.Total.InputTokens,
OutputTokens: combo.Total.OutputTokens,
CacheReadInputTokens: combo.Total.CacheReadInputTokens,
CacheCreationInputTokens: combo.Total.CacheCreationInputTokens,
CostUSD: totalCost,
RequestCount: combo.Total.RequestCount,
MessagesCount: combo.Total.MessagesCount,
InputTokens: combo.Total.InputTokens,
OutputTokens: combo.Total.OutputTokens,
CacheReadInputTokens: combo.Total.CacheReadInputTokens,
CacheCreationInputTokens: combo.Total.CacheCreationInputTokens,
CacheCreation5MinuteInputTokens: combo.Total.CacheCreation5MinuteInputTokens,
CacheCreation1HourInputTokens: combo.Total.CacheCreation1HourInputTokens,
CostUSD: totalCost,
},
ByUser: make(map[string]UsageStatsJSON),
}
@@ -289,13 +368,15 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
result.Costs.ByUser[user] += userCost
comboJSON.ByUser[user] = UsageStatsJSON{
RequestCount: userStats.RequestCount,
MessagesCount: userStats.MessagesCount,
InputTokens: userStats.InputTokens,
OutputTokens: userStats.OutputTokens,
CacheReadInputTokens: userStats.CacheReadInputTokens,
CacheCreationInputTokens: userStats.CacheCreationInputTokens,
CostUSD: userCost,
RequestCount: userStats.RequestCount,
MessagesCount: userStats.MessagesCount,
InputTokens: userStats.InputTokens,
OutputTokens: userStats.OutputTokens,
CacheReadInputTokens: userStats.CacheReadInputTokens,
CacheCreationInputTokens: userStats.CacheCreationInputTokens,
CacheCreation5MinuteInputTokens: userStats.CacheCreation5MinuteInputTokens,
CacheCreation1HourInputTokens: userStats.CacheCreation1HourInputTokens,
CostUSD: userCost,
}
}
@@ -367,7 +448,13 @@ func (u *AggregatedUsage) Save() error {
return err
}
func (u *AggregatedUsage) AddUsage(model string, contextWindow int, messagesCount int, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens int64, user string) error {
func (u *AggregatedUsage) AddUsage(
model string,
contextWindow int,
messagesCount int,
inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64,
user string,
) error {
if model == "" {
return E.New("model cannot be empty")
}
@@ -400,6 +487,10 @@ func (u *AggregatedUsage) AddUsage(model string, contextWindow int, messagesCoun
combo = &u.Combinations[len(u.Combinations)-1]
}
if cacheCreationTokens == 0 {
cacheCreationTokens = cacheCreation5MinuteTokens + cacheCreation1HourTokens
}
// Update total stats
combo.Total.RequestCount++
combo.Total.MessagesCount += messagesCount
@@ -407,6 +498,8 @@ func (u *AggregatedUsage) AddUsage(model string, contextWindow int, messagesCoun
combo.Total.OutputTokens += outputTokens
combo.Total.CacheReadInputTokens += cacheReadTokens
combo.Total.CacheCreationInputTokens += cacheCreationTokens
combo.Total.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens
combo.Total.CacheCreation1HourInputTokens += cacheCreation1HourTokens
// Update per-user stats if user is specified
if user != "" {
@@ -417,6 +510,8 @@ func (u *AggregatedUsage) AddUsage(model string, contextWindow int, messagesCoun
userStats.OutputTokens += outputTokens
userStats.CacheReadInputTokens += cacheReadTokens
userStats.CacheCreationInputTokens += cacheCreationTokens
userStats.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens
userStats.CacheCreation1HourInputTokens += cacheCreation1HourTokens
combo.ByUser[user] = userStats
}