fix: settle Responses cached token usage (#6892)

This commit is contained in:
Qi
2026-08-18 18:24:43 +08:00
committed by GitHub
parent 137d1171f2
commit f116414284
2 changed files with 109 additions and 0 deletions
+23
View File
@@ -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)
+86
View File
@@ -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}),