vibe-proxy/backend/sdk/cliproxy/usage/accounting_test.go
2026-08-24 00:10:41 +02:00

162 lines
6.1 KiB
Go

package usage
import "testing"
func TestNewSubsetTokenBreakdownAvoidsCacheAndReasoningDoubleCount(t *testing.T) {
breakdown := NewSubsetTokenBreakdown(100, 40, 10, 30, 12, 130)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Input.UncachedTokens != 50 || breakdown.Output.NonReasoningTokens != 18 {
t.Fatalf("breakdown = %+v", breakdown)
}
if breakdown.TotalTokens != 130 {
t.Fatalf("total = %d, want 130", breakdown.TotalTokens)
}
}
func TestNewPartialSubsetTokenBreakdownPreservesKnownBuckets(t *testing.T) {
breakdown := NewPartialSubsetTokenBreakdown(10, 4, 0, 0, 0, 15)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Quality != TokenAccountingQualityUnclassified || breakdown.Input.TotalTokens != 10 ||
breakdown.UnclassifiedTokens != 5 {
t.Fatalf("breakdown = %+v", breakdown)
}
}
func TestNewIndependentTokenBreakdownKeepsClaudeCacheBucketsIndependent(t *testing.T) {
breakdown := NewIndependentTokenBreakdown(30, 7, 13, 5, 0, 55)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Input.TotalTokens != 50 || breakdown.TotalTokens != 55 {
t.Fatalf("breakdown = %+v", breakdown)
}
}
func TestNewSeparateReasoningTokenBreakdownAddsReasoningToOutput(t *testing.T) {
breakdown := NewSeparateReasoningTokenBreakdown(20, 5, 0, 7, 3, 30)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Output.TotalTokens != 10 || breakdown.TotalTokens != 30 {
t.Fatalf("breakdown = %+v", breakdown)
}
}
func TestTokenBreakdownMarksContradictoryParentsInconsistent(t *testing.T) {
breakdown := NewSubsetTokenBreakdown(10, 4, 0, 3, 1, 20)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Quality != TokenAccountingQualityInconsistent || breakdown.UnclassifiedTokens != 20 {
t.Fatalf("breakdown = %+v", breakdown)
}
}
func TestNewUnclassifiedTokenBreakdownDoesNotGuessBuckets(t *testing.T) {
breakdown := NewUnclassifiedTokenBreakdown(42)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Quality != TokenAccountingQualityUnclassified || breakdown.UnclassifiedTokens != 42 {
t.Fatalf("breakdown = %+v", breakdown)
}
}
func TestEnsureTokenBreakdownForProviderUsesKnownSemantics(t *testing.T) {
tests := []struct {
name string
provider string
executorType string
detail Detail
wantTotal int64
wantInput int64
wantOutput int64
}{
{
name: "OpenAI subsets cache and reasoning",
provider: "openai",
detail: Detail{InputTokens: 100, OutputTokens: 30, ReasoningTokens: 12, CacheReadTokens: 40, CacheCreationTokens: 10},
wantTotal: 130,
wantInput: 100,
wantOutput: 30,
},
{
name: "OpenAI compatible executor takes precedence",
provider: "anthropic",
executorType: "OpenAICompatExecutor",
detail: Detail{InputTokens: 100, OutputTokens: 30, ReasoningTokens: 12, CacheReadTokens: 40, CacheCreationTokens: 10},
wantTotal: 130,
wantInput: 100,
wantOutput: 30,
},
{
name: "Gemini keeps reasoning separate",
provider: "gemini",
detail: Detail{InputTokens: 100, OutputTokens: 30, ReasoningTokens: 12, CacheReadTokens: 40, CacheCreationTokens: 10},
wantTotal: 142,
wantInput: 100,
wantOutput: 42,
},
{
name: "Claude keeps cache and reasoning independent",
provider: "anthropic",
detail: Detail{InputTokens: 100, OutputTokens: 30, ReasoningTokens: 12, CacheReadTokens: 40, CacheCreationTokens: 10},
wantTotal: 192,
wantInput: 150,
wantOutput: 42,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(tt.detail, tt.provider, tt.executorType)
if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != TokenAccountingQualityComplete {
t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)
}
if detail.TotalTokens != tt.wantTotal || detail.TokenBreakdown.TotalTokens != tt.wantTotal ||
detail.TokenBreakdown.Input.TotalTokens != tt.wantInput || detail.TokenBreakdown.Output.TotalTokens != tt.wantOutput {
t.Fatalf("detail = %+v, want total=%d input=%d output=%d", detail, tt.wantTotal, tt.wantInput, tt.wantOutput)
}
})
}
}
func TestEnsureTokenBreakdownForUnknownProviderDoesNotGuessReasoning(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(Detail{InputTokens: 100, OutputTokens: 30, ReasoningTokens: 12}, "plugin-provider", "")
if detail.TotalTokens != 130 || detail.TokenBreakdown.Quality != TokenAccountingQualityUnclassified || detail.TokenBreakdown.UnclassifiedTokens != 130 {
t.Fatalf("detail = %+v", detail)
}
}
func TestEnsureTokenBreakdownForUnknownProviderPreservesAuxiliaryOnlyUsage(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(Detail{ReasoningTokens: 12, CacheReadTokens: 7}, "plugin-provider", "")
if detail.TotalTokens != 19 || detail.TokenBreakdown.Quality != TokenAccountingQualityUnclassified || detail.TokenBreakdown.UnclassifiedTokens != 19 {
t.Fatalf("detail = %+v", detail)
}
}
func TestEnsureTokenBreakdownForGeminiClassifiesReasoningOnlyUsage(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(Detail{ReasoningTokens: 12}, "gemini", "")
if detail.TotalTokens != 12 || detail.TokenBreakdown.Quality != TokenAccountingQualityComplete ||
detail.TokenBreakdown.Output.ReasoningTokens != 12 {
t.Fatalf("detail = %+v", detail)
}
}
func TestEnsureTokenBreakdownPreservesLegacyCachedOnlyUsage(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(Detail{CachedTokens: 13}, "openai", "")
if detail.TotalTokens != 13 || detail.CacheReadTokens != 13 || detail.TokenBreakdown.Quality != TokenAccountingQualityUnclassified ||
detail.TokenBreakdown.UnclassifiedTokens != 13 {
t.Fatalf("detail = %+v", detail)
}
}
func TestEnsureTokenBreakdownDoesNotOverrideCanonicalZeroCacheRead(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(Detail{CachedTokens: 13, CacheCreationTokens: 13}, "openai", "")
if detail.CacheReadTokens != 0 {
t.Fatalf("detail = %+v", detail)
}
}