From f116414284162ad15d8925f7bca494c109b83e93 Mon Sep 17 00:00:00 2001 From: Qi <108174052+LiaoQi98@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:24:43 +0800 Subject: [PATCH] fix: settle Responses cached token usage (#6892) --- service/billing_usage.go | 23 ++++++++++ service/text_quota_test.go | 86 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/service/billing_usage.go b/service/billing_usage.go index 12656e69..2ea94297 100644 --- a/service/billing_usage.go +++ b/service/billing_usage.go @@ -113,6 +113,29 @@ func usageFromOpenAIBillingUsage(billingUsage *dto.BillingUsage) *dto.Usage { if usage.TotalTokens == 0 { usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens } + if inputDetails := usage.InputTokensDetails; inputDetails != nil { + if usage.PromptTokensDetails.CachedTokens == 0 && inputDetails.CachedTokens > 0 { + usage.PromptTokensDetails.CachedTokens = inputDetails.CachedTokens + } + if usage.PromptTokensDetails.CachedCreationTokens == 0 && inputDetails.CachedCreationTokens > 0 { + usage.PromptTokensDetails.CachedCreationTokens = inputDetails.CachedCreationTokens + } + if usage.PromptTokensDetails.CacheWriteTokens == 0 && inputDetails.CacheWriteTokens > 0 { + usage.PromptTokensDetails.CacheWriteTokens = inputDetails.CacheWriteTokens + } + if usage.PromptTokensDetails.TextTokens == 0 && inputDetails.TextTokens > 0 { + usage.PromptTokensDetails.TextTokens = inputDetails.TextTokens + } + if usage.PromptTokensDetails.ImageTokens == 0 && inputDetails.ImageTokens > 0 { + usage.PromptTokensDetails.ImageTokens = inputDetails.ImageTokens + } + if usage.PromptTokensDetails.AudioTokens == 0 && inputDetails.AudioTokens > 0 { + usage.PromptTokensDetails.AudioTokens = inputDetails.AudioTokens + } + } + if usage.PromptTokensDetails.CachedTokens == 0 && usage.PromptCacheHitTokens > 0 { + usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens + } usage.UsageSemantic = dto.BillingUsageSemanticOpenAI usage.UsageSource = billingUsage.Source usage.BillingUsage = dto.CloneBillingUsage(billingUsage) diff --git a/service/text_quota_test.go b/service/text_quota_test.go index c9e958e7..e4a1ed68 100644 --- a/service/text_quota_test.go +++ b/service/text_quota_test.go @@ -283,6 +283,92 @@ func TestCalculateTextQuotaSummaryUsesOpenAIBillingUsageBeforeTopLevelUsage(t *t require.Equal(t, 98, summary.Quota) } +func TestCalculateTextQuotaSummaryUsesOpenAIResponsesInputTokenDetails(t *testing.T) { + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + relayInfo := &relaycommon.RelayInfo{ + RelayFormat: types.RelayFormatOpenAI, + OriginModelName: "gpt-4o", + PriceData: hosttypes.PriceData{ + ModelRatio: 1, + CompletionRatio: 2, + CacheRatio: 0.25, + GroupRatioInfo: hosttypes.GroupRatioInfo{GroupRatio: 1}, + }, + StartTime: time.Now(), + } + + responsesUsage := &dto.Usage{ + InputTokens: 100, + OutputTokens: 10, + TotalTokens: 110, + InputTokensDetails: &dto.InputTokenDetails{ + CachedTokens: 40, + }, + } + convertedUsage := &dto.Usage{ + PromptTokens: 100, + CompletionTokens: 10, + TotalTokens: 110, + PromptTokensDetails: dto.InputTokenDetails{ + CachedTokens: 40, + }, + BillingUsage: dto.NewOpenAIResponsesBillingUsage(responsesUsage), + } + + effectiveUsage := effectiveBillingUsage(convertedUsage) + require.Equal(t, 40, effectiveUsage.PromptTokensDetails.CachedTokens) + require.Zero(t, convertedUsage.BillingUsage.OpenAIUsage.PromptTokensDetails.CachedTokens) + + summary := calculateTextQuotaSummary(ctx, relayInfo, effectiveUsage) + require.Equal(t, 40, summary.CacheTokens) + // 60 uncached input + 40*0.25 cached input + 10*2 output = 90. + require.Equal(t, 90, summary.Quota) +} + +func TestUsageFromOpenAIBillingUsageNormalizesCacheDetailsWithoutOverwritingCanonicalValues(t *testing.T) { + responsesUsage := &dto.Usage{ + InputTokens: 100, + OutputTokens: 10, + PromptCacheHitTokens: 55, + PromptTokensDetails: dto.InputTokenDetails{ + CachedTokens: 8, + TextTokens: 12, + }, + InputTokensDetails: &dto.InputTokenDetails{ + CachedTokens: 40, + CachedCreationTokens: 5, + CacheWriteTokens: 6, + TextTokens: 60, + ImageTokens: 7, + AudioTokens: 9, + }, + } + + billingUsage := dto.NewOpenAIResponsesBillingUsage(responsesUsage) + usage := effectiveBillingUsage(&dto.Usage{BillingUsage: billingUsage}) + + require.Equal(t, 8, usage.PromptTokensDetails.CachedTokens) + require.Equal(t, 5, usage.PromptTokensDetails.CachedCreationTokens) + require.Equal(t, 6, usage.PromptTokensDetails.CacheWriteTokens) + require.Equal(t, 12, usage.PromptTokensDetails.TextTokens) + require.Equal(t, 7, usage.PromptTokensDetails.ImageTokens) + require.Equal(t, 9, usage.PromptTokensDetails.AudioTokens) + require.Zero(t, billingUsage.OpenAIUsage.PromptTokensDetails.CachedCreationTokens) +} + +func TestUsageFromOpenAIBillingUsageFallsBackToPromptCacheHitTokens(t *testing.T) { + usage := effectiveBillingUsage(&dto.Usage{ + BillingUsage: dto.NewOpenAIChatBillingUsage(&dto.Usage{ + PromptTokens: 100, + CompletionTokens: 10, + PromptCacheHitTokens: 35, + }), + }) + + require.Equal(t, 35, usage.PromptTokensDetails.CachedTokens) +} + func TestUsageBillingPathForLog(t *testing.T) { require.Equal(t, usageBillingPathAnthropic, usageBillingPathForLog(true, &dto.Usage{ BillingUsage: dto.NewClaudeMessagesBillingUsage(&dto.ClaudeUsage{InputTokens: 1}),