merge: adopt main cache billing fixes, keep refactor/ui sidebar badge styling

This commit is contained in:
t0ng7u
2026-07-11 22:42:19 +08:00
8 changed files with 27 additions and 11 deletions
+9 -4
View File
@@ -270,12 +270,17 @@ type InputTokenDetails struct {
// which field the upstream reported it in: Claude-derived conversions populate
// CachedCreationTokens while OpenAI reports cache_write_tokens natively. Both
// are billed at the cache-creation price; when both are present the larger
// value wins so the same tokens are never double-counted.
// value wins so the same tokens are never double-counted. Negative upstream
// values are clamped to zero so they can never lower a charge.
func (d InputTokenDetails) CacheCreationTokensTotal() int {
if d.CacheWriteTokens > d.CachedCreationTokens {
return d.CacheWriteTokens
total := d.CachedCreationTokens
if d.CacheWriteTokens > total {
total = d.CacheWriteTokens
}
return d.CachedCreationTokens
if total < 0 {
return 0
}
return total
}
type OutputTokenDetails struct {
+1
View File
@@ -80,6 +80,7 @@ func normalizeOpenAIUsage(usage *dto.Usage) {
if usage.InputTokensDetails != nil {
usage.PromptTokensDetails.CachedTokens = usage.InputTokensDetails.CachedTokens
usage.PromptTokensDetails.CachedCreationTokens = usage.InputTokensDetails.CachedCreationTokens
usage.PromptTokensDetails.CacheWriteTokens = usage.InputTokensDetails.CacheWriteTokens
usage.PromptTokensDetails.ImageTokens = usage.InputTokensDetails.ImageTokens
usage.PromptTokensDetails.TextTokens = usage.InputTokensDetails.TextTokens
usage.PromptTokensDetails.AudioTokens = usage.InputTokensDetails.AudioTokens
+6
View File
@@ -40,6 +40,11 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
case *dto.OpenAIResponsesRequest:
responsesReq = req
case *dto.OpenAIResponsesCompactionRequest:
// Only fields documented for POST /v1/responses/compact are forwarded:
// model, input, instructions, previous_response_id, prompt_cache_key,
// prompt_cache_options, prompt_cache_retention, service_tier.
// Undocumented Codex-parity fields (tools, reasoning, text) are parsed
// for client compatibility but intentionally not sent upstream.
responsesReq = &dto.OpenAIResponsesRequest{
Model: req.Model,
Input: req.Input,
@@ -47,6 +52,7 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
PreviousResponseID: req.PreviousResponseID,
ParallelToolCalls: req.ParallelToolCalls,
ServiceTier: req.ServiceTier,
PromptCacheKey: req.PromptCacheKey,
PromptCacheOptions: req.PromptCacheOptions,
PromptCacheRetention: req.PromptCacheRetention,
}
@@ -44,7 +44,8 @@ func buildClaudeUsageFromOpenAIUsage(oaiUsage *dto.Usage) *dto.ClaudeUsage {
if oaiUsage.PromptTokensDetails.CacheWriteTokens > 0 {
// OpenAI native cache-write usage counts cached and cache-write tokens
// inside prompt_tokens, while Claude semantics reports input_tokens
// excluding both; the uncached remainder clamps at zero.
// excluding both. Both counts are unadjusted prefixes and may overlap,
// so clamp a negative remainder at zero.
inputTokens = oaiUsage.PromptTokens - oaiUsage.PromptTokensDetails.CachedTokens - cacheCreationTokens
if inputTokens < 0 {
inputTokens = 0
@@ -92,7 +92,7 @@ func TestBuildClaudeUsageFromOpenAICacheWriteUsage(t *testing.T) {
require.NotNil(t, usage)
// Claude semantics reports input_tokens excluding cache read/write; the
// remainder 3619-2921-3616 clamps to 0.
// overlapping unadjusted prefixes drive the remainder negative, clamp to 0.
assert.Equal(t, 0, usage.InputTokens)
assert.Equal(t, 2921, usage.CacheReadInputTokens)
assert.Equal(t, 3616, usage.CacheCreationInputTokens)
+4 -3
View File
@@ -294,9 +294,10 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf
}
}
// OpenAI cache-write usage can report cached_tokens + cache_write_tokens
// exceeding prompt_tokens; the uncached remainder must clamp at zero so
// billing never subtracts more than the reported input.
// OpenAI cache-write usage reports unadjusted prefix counts, so
// cached_tokens + cache_write_tokens can exceed prompt_tokens and the
// remainder can go negative. Clamp at zero so overlap never turns into
// a negative base charge.
if baseTokens.IsNegative() {
baseTokens = decimal.Zero
}
+2 -2
View File
@@ -411,8 +411,8 @@ func TestCalculateTextQuotaSummaryBillsOpenAICacheWriteTokens(t *testing.T) {
t.Run("uncached remainder clamps to zero", func(t *testing.T) {
// Real OpenAI payload shape: cached_tokens + cache_write_tokens exceeds
// prompt_tokens, so the uncached remainder must clamp to 0 instead of
// producing a negative charge component.
// prompt_tokens because both are unadjusted prefix counts. The negative
// remainder must clamp to zero, never turn into a negative base charge.
usage := &dto.Usage{
PromptTokens: 3619,
CompletionTokens: 36,
+2
View File
@@ -67,6 +67,8 @@ func BuildTieredTokenParams(usage *dto.Usage, isClaudeUsageSemantic bool, usedVa
}
}
// OpenAI cache-write usage reports unadjusted prefix counts, so cr + cc can
// exceed the prompt and drive the remainder negative. Clamp at zero.
if p < 0 {
p = 0
}