Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ func buildClaudeUsageFromOpenAIUsage(oaiUsage *dto.Usage) *dto.ClaudeUsage {
)
cacheCreationTokens := oaiUsage.PromptTokensDetails.CacheCreationTokensTotal()
inputTokens := oaiUsage.PromptTokens
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. Both counts are unadjusted prefixes and may overlap,
// so clamp a negative remainder at zero.
// OpenAI native cache-write / cache-read usage counts both cached and
// cache-creation tokens inside prompt_tokens, while Claude semantics
// reports input_tokens excluding both. Deduplicate so that the sum
// `input_tokens + cache_creation_input_tokens + cache_read_input_tokens`
// equals the true total context size rather than double-counting cached
// tokens (§ref HsMirage, issue #4395).
// - CacheWriteTokens > 0: openai cache-write route (original guard)
// - CachedTokens > 0: pure cache-read (bypassed the old guard)
if oaiUsage.PromptTokensDetails.CacheWriteTokens > 0 || oaiUsage.PromptTokensDetails.CachedTokens > 0 {
inputTokens = oaiUsage.PromptTokens - oaiUsage.PromptTokensDetails.CachedTokens - cacheCreationTokens
if inputTokens < 0 {
inputTokens = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,31 @@ func TestBuildClaudeUsageFromOpenAICacheWriteUsage(t *testing.T) {
assert.Equal(t, 3616, usage.BillingUsage.OpenAIUsage.PromptTokensDetails.CacheWriteTokens)
}

func TestBuildClaudeUsageFromOpenAIPureCacheRead(t *testing.T) {
// HsMirage's pure cache-read reproduction (issue #4395):
// second identical request hits cache; CacheWriteTokens==0, CachedTokens>0.
usage := buildClaudeUsageFromOpenAIUsage(&dto.Usage{
PromptTokens: 15305,
CompletionTokens: 5,
TotalTokens: 15310,
PromptTokensDetails: dto.InputTokenDetails{
CachedTokens: 15104,
CacheWriteTokens: 0,
},
})

require.NotNil(t, usage)
// input_tokens must exclude cached: 15305 - 15104 = 201
assert.Equal(t, 201, usage.InputTokens)
assert.Equal(t, 15104, usage.CacheReadInputTokens)
assert.Equal(t, 0, usage.CacheCreationInputTokens)
assert.Equal(t, 5, usage.OutputTokens)
// Total context = 201 + 15104 = 15305 (not double-counted)
require.NotNil(t, usage.BillingUsage)
require.NotNil(t, usage.BillingUsage.OpenAIUsage)
assert.Equal(t, 15305, usage.BillingUsage.OpenAIUsage.PromptTokens)
}

func TestStreamResponseOpenAI2ClaudeClosesTextThinkingAndToolBlocks(t *testing.T) {
info := &relaycommon.RelayInfo{
ClaudeConvertInfo: &relaycommon.ClaudeConvertInfo{
Expand Down