diff --git a/controllers/ai.go b/controllers/ai.go index c537fb1..ebb17b1 100644 --- a/controllers/ai.go +++ b/controllers/ai.go @@ -245,6 +245,8 @@ type aiAnalysisResult struct { Content string InputTokens int OutputTokens int + CacheHitTokens int + CacheMissTokens int InputSizeBytes int OutputSizeBytes int } @@ -289,10 +291,16 @@ func callAIForAnalysis(prompt string) (*aiAnalysisResult, error) { } content := resp.Choices[0].Message.Content + cacheHitTokens := 0 + if resp.Usage.PromptTokensDetails != nil { + cacheHitTokens = resp.Usage.PromptTokensDetails.CachedTokens + } return &aiAnalysisResult{ Content: content, InputTokens: resp.Usage.PromptTokens, OutputTokens: resp.Usage.CompletionTokens, + CacheHitTokens: cacheHitTokens, + CacheMissTokens: resp.Usage.PromptTokens - cacheHitTokens, InputSizeBytes: len(prompt), OutputSizeBytes: len(content), }, nil @@ -397,17 +405,29 @@ func (tc *TrainingController) AnalyzeByAI(c *gin.Context) { var costJSON string var totalCost float64 if err := config.DB.First(&pricing).Error; err == nil { - inputCost := float64(analysisResult.InputTokens) * pricing.InputPricePerMillion / 1_000_000 + cacheMissPrice := pricing.CacheMissPricePerMillion + if cacheMissPrice == 0 { + cacheMissPrice = pricing.InputPricePerMillion + } + cacheHitPrice := pricing.CacheHitPricePerMillion + if cacheHitPrice == 0 { + cacheHitPrice = pricing.InputPricePerMillion + } + cacheHitCost := float64(analysisResult.CacheHitTokens) * cacheHitPrice / 1_000_000 + cacheMissCost := float64(analysisResult.CacheMissTokens) * cacheMissPrice / 1_000_000 outputCost := float64(analysisResult.OutputTokens) * pricing.OutputPricePerMillion / 1_000_000 - totalCost = inputCost + outputCost + totalCost = cacheHitCost + cacheMissCost + outputCost costInfo := map[string]interface{}{ - "pricingName": pricing.Name, - "provider": pricing.Provider, - "inputPricePerMillion": pricing.InputPricePerMillion, - "outputPricePerMillion": pricing.OutputPricePerMillion, - "inputCost": inputCost, - "outputCost": outputCost, + "pricingName": pricing.Name, + "provider": pricing.Provider, + "inputPricePerMillion": pricing.InputPricePerMillion, + "cacheHitPricePerMillion": cacheHitPrice, + "cacheMissPricePerMillion": cacheMissPrice, + "outputPricePerMillion": pricing.OutputPricePerMillion, + "cacheHitCost": cacheHitCost, + "cacheMissCost": cacheMissCost, + "outputCost": outputCost, } if b, err := json.Marshal(costInfo); err == nil { costJSON = string(b) @@ -423,6 +443,8 @@ func (tc *TrainingController) AnalyzeByAI(c *gin.Context) { TotalCost: totalCost, InputTokens: analysisResult.InputTokens, OutputTokens: analysisResult.OutputTokens, + CacheHitTokens: analysisResult.CacheHitTokens, + CacheMissTokens: analysisResult.CacheMissTokens, InputSizeBytes: analysisResult.InputSizeBytes, OutputSizeBytes: analysisResult.OutputSizeBytes, DurationMs: durationMs, diff --git a/controllers/statistics.go b/controllers/statistics.go index 556a0d6..7d8feca 100644 --- a/controllers/statistics.go +++ b/controllers/statistics.go @@ -117,6 +117,8 @@ type regionStatisticsItem struct { Count int64 `json:"count"` TotalInputTokens int64 `json:"totalInputTokens"` TotalOutputTokens int64 `json:"totalOutputTokens"` + TotalCacheHitTokens int64 `json:"totalCacheHitTokens"` + TotalCacheMissTokens int64 `json:"totalCacheMissTokens"` TotalInputSizeBytes int64 `json:"totalInputSizeBytes"` TotalOutputSizeBytes int64 `json:"totalOutputSizeBytes"` TotalDurationMs int64 `json:"totalDurationMs"` @@ -127,6 +129,8 @@ type regionStatisticsItem struct { TotalCost float64 `json:"totalCost"` TotalInputCost float64 `json:"totalInputCost"` TotalOutputCost float64 `json:"totalOutputCost"` + TotalCacheHitCost float64 `json:"totalCacheHitCost"` + TotalCacheMissCost float64 `json:"totalCacheMissCost"` AnalysisTypeCounts map[string]int64 `json:"analysisTypeCounts"` SourceTypeCounts map[string]int64 `json:"sourceTypeCounts"` FirstUsedAt *time.Time `json:"firstUsedAt"` @@ -160,6 +164,8 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { Count int64 TotalInputTokens int64 TotalOutputTokens int64 + TotalCacheHitTokens int64 + TotalCacheMissTokens int64 TotalInputSizeBytes int64 TotalOutputSizeBytes int64 TotalDurationMs int64 @@ -168,6 +174,8 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { TotalCost float64 TotalInputCost float64 TotalOutputCost float64 + TotalCacheHitCost float64 + TotalCacheMissCost float64 FirstUsedAt *int64 LastUsedAt *int64 } @@ -178,14 +186,18 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { COUNT(*) as count, COALESCE(SUM(input_tokens), 0) as total_input_tokens, COALESCE(SUM(output_tokens), 0) as total_output_tokens, + COALESCE(SUM(cache_hit_tokens), 0) as total_cache_hit_tokens, + COALESCE(SUM(cache_miss_tokens), 0) as total_cache_miss_tokens, COALESCE(SUM(input_size_bytes), 0) as total_input_size_bytes, COALESCE(SUM(output_size_bytes), 0) as total_output_size_bytes, COALESCE(SUM(duration_ms), 0) as total_duration_ms, COALESCE(SUM(original_file_size), 0) as total_original_file_size, COALESCE(SUM(compressed_content_size), 0) as total_compressed_size, COALESCE(SUM(total_cost), 0) as total_cost, - COALESCE(SUM((cost_json::jsonb->>'inputCost')::float8), 0) as total_input_cost, + COALESCE(SUM((cost_json::jsonb->>'cacheHitCost')::float8 + (cost_json::jsonb->>'cacheMissCost')::float8), 0) as total_input_cost, COALESCE(SUM((cost_json::jsonb->>'outputCost')::float8), 0) as total_output_cost, + COALESCE(SUM((cost_json::jsonb->>'cacheHitCost')::float8), 0) as total_cache_hit_cost, + COALESCE(SUM((cost_json::jsonb->>'cacheMissCost')::float8), 0) as total_cache_miss_cost, MIN(upload_time) as first_used_at, MAX(upload_time) as last_used_at `).Group("region_id").Scan(&rawResults).Error @@ -298,6 +310,8 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { Count: r.Count, TotalInputTokens: r.TotalInputTokens, TotalOutputTokens: r.TotalOutputTokens, + TotalCacheHitTokens: r.TotalCacheHitTokens, + TotalCacheMissTokens: r.TotalCacheMissTokens, TotalInputSizeBytes: r.TotalInputSizeBytes, TotalOutputSizeBytes: r.TotalOutputSizeBytes, TotalDurationMs: r.TotalDurationMs, @@ -308,6 +322,8 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { TotalCost: r.TotalCost, TotalInputCost: r.TotalInputCost, TotalOutputCost: r.TotalOutputCost, + TotalCacheHitCost: r.TotalCacheHitCost, + TotalCacheMissCost: r.TotalCacheMissCost, AnalysisTypeCounts: analysisTypeMap[regionID], SourceTypeCounts: sourceTypeMap[regionID], FirstUsedAt: firstUsedAt, @@ -319,6 +335,8 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { overall.Count += r.Count overall.TotalInputTokens += r.TotalInputTokens overall.TotalOutputTokens += r.TotalOutputTokens + overall.TotalCacheHitTokens += r.TotalCacheHitTokens + overall.TotalCacheMissTokens += r.TotalCacheMissTokens overall.TotalInputSizeBytes += r.TotalInputSizeBytes overall.TotalOutputSizeBytes += r.TotalOutputSizeBytes overall.TotalDurationMs += r.TotalDurationMs @@ -327,6 +345,8 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { overall.TotalCost += r.TotalCost overall.TotalInputCost += r.TotalInputCost overall.TotalOutputCost += r.TotalOutputCost + overall.TotalCacheHitCost += r.TotalCacheHitCost + overall.TotalCacheMissCost += r.TotalCacheMissCost if firstUsedAt != nil { if overall.FirstUsedAt == nil || firstUsedAt.Before(*overall.FirstUsedAt) { @@ -627,20 +647,24 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) { } type timelineItem struct { - Date string `json:"date"` - Count int64 `json:"count"` - InputTokens int64 `json:"inputTokens"` - OutputTokens int64 `json:"outputTokens"` - TotalCost float64 `json:"totalCost"` + Date string `json:"date"` + Count int64 `json:"count"` + InputTokens int64 `json:"inputTokens"` + OutputTokens int64 `json:"outputTokens"` + CacheHitTokens int64 `json:"cacheHitTokens"` + CacheMissTokens int64 `json:"cacheMissTokens"` + TotalCost float64 `json:"totalCost"` } type rawRegionTimeline struct { - RegionID *uint32 - Date string - Count int64 - InputTokens int64 - OutputTokens int64 - TotalCost float64 + RegionID *uint32 + Date string + Count int64 + InputTokens int64 + OutputTokens int64 + CacheHitTokens int64 + CacheMissTokens int64 + TotalCost float64 } var rawResults []rawRegionTimeline @@ -650,6 +674,8 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) { COUNT(*) as count, COALESCE(SUM(input_tokens), 0) as input_tokens, COALESCE(SUM(output_tokens), 0) as output_tokens, + COALESCE(SUM(cache_hit_tokens), 0) as cache_hit_tokens, + COALESCE(SUM(cache_miss_tokens), 0) as cache_miss_tokens, COALESCE(SUM(total_cost), 0) as total_cost `).Group("region_id, DATE(TO_TIMESTAMP(upload_time / 1000.0))").Order("region_id, date ASC").Scan(&rawResults).Error @@ -670,6 +696,8 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) { overallMap[r.Date].Count += r.Count overallMap[r.Date].InputTokens += r.InputTokens overallMap[r.Date].OutputTokens += r.OutputTokens + overallMap[r.Date].CacheHitTokens += r.CacheHitTokens + overallMap[r.Date].CacheMissTokens += r.CacheMissTokens overallMap[r.Date].TotalCost += r.TotalCost regionID := uint32(0) @@ -678,11 +706,13 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) { } regionIDStr := strconv.FormatUint(uint64(regionID), 10) regionItemsMap[regionIDStr] = append(regionItemsMap[regionIDStr], timelineItem{ - Date: r.Date, - Count: r.Count, - InputTokens: r.InputTokens, - OutputTokens: r.OutputTokens, - TotalCost: r.TotalCost, + Date: r.Date, + Count: r.Count, + InputTokens: r.InputTokens, + OutputTokens: r.OutputTokens, + CacheHitTokens: r.CacheHitTokens, + CacheMissTokens: r.CacheMissTokens, + TotalCost: r.TotalCost, }) if _, ok := regionIDSet[regionID]; !ok && regionID > 0 { regionIDSet[regionID] = struct{}{} diff --git a/models/analyze.go b/models/analyze.go index 353ee49..82e8336 100644 --- a/models/analyze.go +++ b/models/analyze.go @@ -16,6 +16,8 @@ type AIAnalysisRecord struct { OutputTokens int `json:"outputTokens"` InputSizeBytes int `json:"inputSizeBytes"` OutputSizeBytes int `json:"outputSizeBytes"` + CacheHitTokens int `json:"cacheHitTokens"` + CacheMissTokens int `json:"cacheMissTokens"` DurationMs int64 `json:"durationMs"` OriginalFileSize int64 `json:"originalFileSize"` CompressedContentSize int64 `json:"compressedContentSize"` diff --git a/models/pricing.go b/models/pricing.go index ad6919e..3e93c53 100644 --- a/models/pricing.go +++ b/models/pricing.go @@ -6,10 +6,12 @@ import ( type AIPricingConfig struct { gorm.Model - Name string `gorm:"size:64;uniqueIndex" json:"name"` - Provider string `gorm:"size:64" json:"provider"` - InputPricePerMillion float64 `json:"inputPricePerMillion"` - OutputPricePerMillion float64 `json:"outputPricePerMillion"` + Name string `gorm:"size:64;uniqueIndex" json:"name"` + Provider string `gorm:"size:64" json:"provider"` + InputPricePerMillion float64 `json:"inputPricePerMillion"` + OutputPricePerMillion float64 `json:"outputPricePerMillion"` + CacheHitPricePerMillion float64 `json:"cacheHitPricePerMillion"` + CacheMissPricePerMillion float64 `json:"cacheMissPricePerMillion"` } func EnsureDefaultAIPricing(db *gorm.DB) error { @@ -21,9 +23,11 @@ func EnsureDefaultAIPricing(db *gorm.DB) error { return nil } return db.Create(&AIPricingConfig{ - Name: "deepseek-v4-flash", - Provider: "tencentmaas", - InputPricePerMillion: 1, - OutputPricePerMillion: 2, + Name: "deepseek-v4-flash", + Provider: "tencentmaas", + InputPricePerMillion: 1, + CacheHitPricePerMillion: 1, + CacheMissPricePerMillion: 0.02, + OutputPricePerMillion: 2, }).Error }