1181 lines · 45.0 KB · Go
RawDownload
package helps import (	"context"	"errors"	"fmt"	"io"	"net/http"	"net/url"	"strings"	"testing"	"time" 	"github.com/router-for-me/CLIProxyAPI/v7/internal/clienterror"	"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"	cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"	cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"	"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage") func TestParseOpenAIUsageChatCompletions(t *testing.T) {	data := []byte(`{"usage":{"prompt_tokens":10,"completion_tokens":6,"total_tokens":16,"prompt_tokens_details":{"cached_tokens":4},"completion_tokens_details":{"reasoning_tokens":5}}}`)	detail := ParseOpenAIUsage(data)	if detail.InputTokens != 10 {		t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 10)	}	if detail.OutputTokens != 6 {		t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 6)	}	if detail.TotalTokens != 16 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 16)	}	if detail.CachedTokens != 4 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 4)	}	if detail.CacheReadTokens != 4 {		t.Fatalf("cache read tokens = %d, want %d", detail.CacheReadTokens, 4)	}	if detail.ReasoningTokens != 5 {		t.Fatalf("reasoning tokens = %d, want %d", detail.ReasoningTokens, 5)	}	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != usage.TokenAccountingQualityComplete {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}	if detail.TokenBreakdown.Input.UncachedTokens != 6 || detail.TokenBreakdown.Output.NonReasoningTokens != 1 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseOpenAIUsageResponses(t *testing.T) {	data := []byte(`{"service_tier":"default","usage":{"input_tokens":10,"output_tokens":20,"total_tokens":30,"input_tokens_details":{"cached_tokens":7},"output_tokens_details":{"reasoning_tokens":9}}}`)	detail := ParseOpenAIUsage(data)	if detail.InputTokens != 10 {		t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 10)	}	if detail.OutputTokens != 20 {		t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 20)	}	if detail.TotalTokens != 30 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 30)	}	if detail.CachedTokens != 7 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 7)	}	if detail.CacheReadTokens != 7 {		t.Fatalf("cache read tokens = %d, want %d", detail.CacheReadTokens, 7)	}	if detail.ReasoningTokens != 9 {		t.Fatalf("reasoning tokens = %d, want %d", detail.ReasoningTokens, 9)	}	if detail.ResponseServiceTier != "default" {		t.Fatalf("response service tier = %q, want default", detail.ResponseServiceTier)	}	if detail.TokenBreakdown.Input.UncachedTokens != 3 || detail.TokenBreakdown.Output.NonReasoningTokens != 11 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseOpenAIUsageTotalOnlyIsUnclassified(t *testing.T) {	detail := ParseOpenAIUsage([]byte(`{"usage":{"total_tokens":42}}`))	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != usage.TokenAccountingQualityUnclassified ||		detail.TotalTokens != 42 || detail.TokenBreakdown.UnclassifiedTokens != 42 {		t.Fatalf("detail = %+v", detail)	}} func TestParseOpenAIUsagePartialBucketsPreserveKnownTokens(t *testing.T) {	detail := ParseOpenAIUsage([]byte(`{"usage":{"input_tokens":10,"total_tokens":15}}`))	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != usage.TokenAccountingQualityUnclassified ||		detail.TokenBreakdown.Input.TotalTokens != 10 || detail.TokenBreakdown.UnclassifiedTokens != 5 {		t.Fatalf("detail = %+v", detail)	}} func TestParseOpenAIUsageExplicitZeroBucketsRemainInconsistent(t *testing.T) {	detail := ParseOpenAIUsage([]byte(`{"usage":{"input_tokens":0,"output_tokens":0,"total_tokens":42}}`))	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != usage.TokenAccountingQualityInconsistent {		t.Fatalf("detail = %+v", detail)	}} func TestParseCodexUsageIncludesCacheWriteTokens(t *testing.T) {	data := []byte(`{"response":{"service_tier":"priority","usage":{"input_tokens":100,"output_tokens":20,"total_tokens":120,"input_tokens_details":{"cached_tokens":30,"cache_write_tokens":40}}}}`)	detail, ok := ParseCodexUsage(data)	if !ok {		t.Fatal("ParseCodexUsage() ok = false, want true")	}	if detail.InputTokens != 100 {		t.Fatalf("input tokens = %d, want 100", detail.InputTokens)	}	if detail.OutputTokens != 20 {		t.Fatalf("output tokens = %d, want 20", detail.OutputTokens)	}	if detail.CachedTokens != 30 {		t.Fatalf("cached tokens = %d, want 30", detail.CachedTokens)	}	if detail.CacheReadTokens != 30 {		t.Fatalf("cache read tokens = %d, want 30", detail.CacheReadTokens)	}	if detail.CacheCreationTokens != 40 {		t.Fatalf("cache creation tokens = %d, want 40", detail.CacheCreationTokens)	}	if detail.TotalTokens != 120 {		t.Fatalf("total tokens = %d, want 120", detail.TotalTokens)	}	if detail.ResponseServiceTier != "priority" {		t.Fatalf("response service tier = %q, want priority", detail.ResponseServiceTier)	}	if detail.TokenBreakdown.Input.UncachedTokens != 30 || detail.TokenBreakdown.Input.CacheWriteTokens != 40 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseOpenAIUsageNormalizesCacheCreationAlias(t *testing.T) {	data := []byte(`{"usage":{"input_tokens":10,"output_tokens":2,"total_tokens":12,"input_tokens_details":{"cache_creation_tokens":4}}}`)	detail := ParseOpenAIUsage(data)	if detail.CacheCreationTokens != 4 {		t.Fatalf("cache creation tokens = %d, want 4", detail.CacheCreationTokens)	}} func TestParseOpenAIUsageIgnoresNullUsage(t *testing.T) {	data := []byte(`{"usage":null}`)	detail := ParseOpenAIUsage(data)	if detail != (usage.Detail{}) {		t.Fatalf("detail = %+v, want zero detail", detail)	}} func TestParseOpenAIUsagePreservesResponseTierWithoutUsage(t *testing.T) {	t.Parallel() 	detail := ParseOpenAIUsage([]byte(`{"service_tier":"default"}`))	if detail.ResponseServiceTier != "default" {		t.Fatalf("response service tier = %q, want default", detail.ResponseServiceTier)	}} func TestParseCodexUsagePreservesResponseTierWithoutUsage(t *testing.T) {	t.Parallel() 	detail, ok := ParseCodexUsage([]byte(`{"response":{"service_tier":"default"}}`))	if !ok || detail.ResponseServiceTier != "default" {		t.Fatalf("ParseCodexUsage() = (%+v, %v), want response tier default", detail, ok)	}} func TestParseOpenAIStreamUsageIgnoresNullUsage(t *testing.T) {	line := []byte(`data: {"id":"chunk_1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"hi"},"finish_reason":null}],"usage":null}`)	if detail, ok := ParseOpenAIStreamUsage(line); ok {		t.Fatalf("ParseOpenAIStreamUsage() = (%+v, true), want false for null usage", detail)	}} func TestParseOpenAIStreamUsageResponsesFields(t *testing.T) {	line := []byte(`data: {"id":"chunk_1","object":"chat.completion.chunk","service_tier":"flex","choices":[],"usage":{"input_tokens":8,"output_tokens":5,"total_tokens":13,"input_tokens_details":{"cached_tokens":3},"output_tokens_details":{"reasoning_tokens":2}}}`)	detail, ok := ParseOpenAIStreamUsage(line)	if !ok {		t.Fatal("ParseOpenAIStreamUsage() ok = false, want true")	}	if detail.InputTokens != 8 {		t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 8)	}	if detail.OutputTokens != 5 {		t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 5)	}	if detail.TotalTokens != 13 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 13)	}	if detail.CachedTokens != 3 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 3)	}	if detail.CacheReadTokens != 3 {		t.Fatalf("cache read tokens = %d, want %d", detail.CacheReadTokens, 3)	}	if detail.ReasoningTokens != 2 {		t.Fatalf("reasoning tokens = %d, want %d", detail.ReasoningTokens, 2)	}	if detail.ResponseServiceTier != "flex" {		t.Fatalf("response service tier = %q, want flex", detail.ResponseServiceTier)	}} func TestStreamUsageBufferKeepsLastUsage(t *testing.T) {	var buffer StreamUsageBuffer	buffer.Observe(usage.Detail{}, true)	buffer.Observe(usage.Detail{InputTokens: 1, OutputTokens: 1, TotalTokens: 2}, false)	buffer.Observe(usage.Detail{InputTokens: 39320, OutputTokens: 26, TotalTokens: 39346, CachedTokens: 33280}, true) 	detail, ok := buffer.Detail()	if !ok {		t.Fatal("buffer detail ok = false, want true")	}	if detail.InputTokens != 39320 {		t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 39320)	}	if detail.OutputTokens != 26 {		t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 26)	}	if detail.TotalTokens != 39346 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 39346)	}	if detail.CachedTokens != 33280 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 33280)	}} func TestStreamUsageBufferPreservesTierAcrossChunks(t *testing.T) {	t.Parallel() 	var buffer StreamUsageBuffer	buffer.ObserveOpenAIStream([]byte(`data: {"service_tier":"default"}`))	buffer.ObserveOpenAIStream([]byte(`data: {"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}`))	detail, ok := buffer.Detail()	if !ok {		t.Fatal("Detail() ok = false, want true")	}	if detail.InputTokens != 1 || detail.OutputTokens != 1 || detail.ResponseServiceTier != "default" {		t.Fatalf("detail = %+v, want usage with response tier default", detail)	}} func TestStreamUsageBufferObserveOpenAIStreamStateTransitions(t *testing.T) {	t.Parallel() 	t.Run("same chunk", func(t *testing.T) {		var buffer StreamUsageBuffer		buffer.ObserveOpenAIStream([]byte(`data: {"service_tier":"flex","usage":{"input_tokens":2,"output_tokens":3,"total_tokens":5}}`))		detail, ok := buffer.Detail()		if !ok || detail.InputTokens != 2 || detail.ResponseServiceTier != "flex" {			t.Fatalf("detail = %+v ok=%v", detail, ok)		}	}) 	t.Run("usage before tier", func(t *testing.T) {		var buffer StreamUsageBuffer		buffer.ObserveOpenAIStream([]byte(`data: {"usage":{"input_tokens":2,"output_tokens":3,"total_tokens":5}}`))		buffer.ObserveOpenAIStream([]byte(`data: {"service_tier":"default"}`))		detail, ok := buffer.Detail()		if !ok || detail.InputTokens != 2 || detail.ResponseServiceTier != "default" {			t.Fatalf("detail = %+v ok=%v", detail, ok)		}	}) 	t.Run("final usage tier overrides early tier", func(t *testing.T) {		var buffer StreamUsageBuffer		buffer.ObserveOpenAIStream([]byte(`data: {"service_tier":"default"}`))		buffer.ObserveOpenAIStream([]byte(`data: {"service_tier":"priority","usage":{"input_tokens":2,"output_tokens":3,"total_tokens":5}}`))		detail, ok := buffer.Detail()		if !ok || detail.ResponseServiceTier != "priority" {			t.Fatalf("detail = %+v ok=%v", detail, ok)		}	}) 	t.Run("irrelevant and invalid chunks do not change state", func(t *testing.T) {		var buffer StreamUsageBuffer		buffer.ObserveOpenAIStream([]byte(`data: {"content":"the word \"usage\" appears here"}`))		buffer.ObserveOpenAIStream([]byte(`data: {"usage":`))		buffer.ObserveOpenAIStream([]byte(`data: {"usage":null}`))		if detail, ok := buffer.Detail(); ok {			t.Fatalf("detail = %+v ok=true, want empty buffer", detail)		}	}) 	t.Run("zero token usage is retained", func(t *testing.T) {		var buffer StreamUsageBuffer		buffer.ObserveOpenAIStream([]byte(`data: {"usage":{"input_tokens":0,"output_tokens":0,"total_tokens":0}}`))		if _, ok := buffer.Detail(); !ok {			t.Fatal("Detail() ok = false, want true")		}	})} func TestStreamUsageBufferPreservesOnlyZeroUsage(t *testing.T) {	var buffer StreamUsageBuffer	buffer.Observe(usage.Detail{}, true) 	detail, ok := buffer.Detail()	if !ok {		t.Fatal("buffer detail ok = false, want true")	}	if detail != (usage.Detail{}) {		t.Fatalf("detail = %+v, want zero detail", detail)	}} func TestParseClaudeUsageIncludesCacheTokensInTotal(t *testing.T) {	data := []byte(`{"usage":{"input_tokens":3085,"output_tokens":253,"cache_read_input_tokens":7,"cache_creation_input_tokens":19514}}`)	detail := ParseClaudeUsage(data)	if detail.InputTokens != 3085 {		t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 3085)	}	if detail.OutputTokens != 253 {		t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 253)	}	if detail.CacheReadTokens != 7 {		t.Fatalf("cache read tokens = %d, want %d", detail.CacheReadTokens, 7)	}	if detail.CacheCreationTokens != 19514 {		t.Fatalf("cache creation tokens = %d, want %d", detail.CacheCreationTokens, 19514)	}	if detail.CachedTokens != 7 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 7)	}	if detail.TotalTokens != 22859 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 22859)	}	if detail.TokenBreakdown.Input.TotalTokens != 22606 || detail.TokenBreakdown.Input.UncachedTokens != 3085 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseClaudeUsageFallsBackCachedTokensToCacheCreation(t *testing.T) {	data := []byte(`{"usage":{"input_tokens":3085,"output_tokens":253,"cache_creation_input_tokens":19514}}`)	detail := ParseClaudeUsage(data)	if detail.CachedTokens != 19514 {		t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 19514)	}	if detail.TotalTokens != 22852 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 22852)	}} func TestParseClaudeUsagePreservesThinkingTokensAsReasoningSubset(t *testing.T) {	// Sanitized shape from local Anthropic request logs under ~/.config/cpa/logs.	data := []byte(`{"usage":{"input_tokens":2,"cache_creation_input_tokens":831,"cache_read_input_tokens":44225,"output_tokens":244,"output_tokens_details":{"thinking_tokens":40}}}`)	detail := ParseClaudeUsage(data)	if detail.OutputTokens != 244 {		t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 244)	}	if detail.ReasoningTokens != 40 {		t.Fatalf("reasoning tokens = %d, want %d", detail.ReasoningTokens, 40)	}	if detail.TotalTokens != 45302 {		t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 45302)	}	if !detail.TokenBreakdown.Valid() ||		detail.TokenBreakdown.Output.TotalTokens != 244 ||		detail.TokenBreakdown.Output.NonReasoningTokens != 204 ||		detail.TokenBreakdown.Output.ReasoningTokens != 40 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseClaudeStreamUsagePreservesThinkingTokensAsReasoningSubset(t *testing.T) {	line := []byte(`data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":2,"cache_creation_input_tokens":831,"cache_read_input_tokens":44225,"output_tokens":244,"output_tokens_details":{"thinking_tokens":40}}}`)	detail, ok := ParseClaudeStreamUsage(line)	if !ok {		t.Fatal("expected stream usage to parse")	}	if detail.OutputTokens != 244 || detail.ReasoningTokens != 40 || detail.TotalTokens != 45302 {		t.Fatalf("stream usage detail = %+v", detail)	}	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Output.NonReasoningTokens != 204 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseClaudeStreamUsage_MessageStart(t *testing.T) {	line := []byte(`data: {"type":"message_start","message":{"id":"msg_123","type":"message","role":"assistant","content":[],"model":"claude-opus-5","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2095,"cache_creation_input_tokens":7185,"cache_read_input_tokens":355598,"output_tokens":1}}}`)	detail, ok := ParseClaudeStreamUsage(line)	if !ok {		t.Fatal("expected stream usage to parse from message_start")	}	if detail.InputTokens != 2095 {		t.Errorf("input tokens = %d, want 2095", detail.InputTokens)	}	if detail.CacheReadTokens != 355598 {		t.Errorf("cache read tokens = %d, want 355598", detail.CacheReadTokens)	}	if detail.CacheCreationTokens != 7185 {		t.Errorf("cache creation tokens = %d, want 7185", detail.CacheCreationTokens)	}	if detail.CachedTokens != 355598 {		t.Errorf("cached tokens = %d, want 355598", detail.CachedTokens)	}} func TestParseClaudeUsageFallsBackToTopLevelThinkingTokens(t *testing.T) {	data := []byte(`{"usage":{"input_tokens":3,"output_tokens":10,"thinking_tokens":4}}`)	detail := ParseClaudeUsage(data)	if detail.OutputTokens != 10 || detail.ReasoningTokens != 4 || detail.TotalTokens != 13 {		t.Fatalf("detail = %+v", detail)	}	if detail.TokenBreakdown.Output.NonReasoningTokens != 6 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseGeminiUsageNormalizesCachedContent(t *testing.T) {	detail := ParseGeminiUsage([]byte(`{"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":2,"cachedContentTokenCount":4,"totalTokenCount":12}}`))	if detail.CachedTokens != 4 {		t.Fatalf("cached tokens = %d, want 4", detail.CachedTokens)	}	if detail.CacheReadTokens != 4 {		t.Fatalf("cache read tokens = %d, want 4", detail.CacheReadTokens)	}	if detail.TokenBreakdown.Input.UncachedTokens != 6 || detail.TokenBreakdown.TotalTokens != 12 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseGeminiUsageIncludesToolUsePromptTokens(t *testing.T) {	detail := ParseGeminiUsage([]byte(`{"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":2,"thoughtsTokenCount":3,"toolUsePromptTokenCount":5,"totalTokenCount":20}}`))	if detail.InputTokens != 15 || detail.TotalTokens != 20 {		t.Fatalf("detail = %+v", detail)	}	if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != usage.TokenAccountingQualityComplete ||		detail.TokenBreakdown.Input.UncachedTokens != 15 || detail.TokenBreakdown.Output.ReasoningTokens != 3 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestParseGeminiStreamUsageSkipsZeroPlaceholder(t *testing.T) {	lines := [][]byte{		[]byte(`data: {"usageMetadata":{"promptTokenCount":0,"candidatesTokenCount":0,"thoughtsTokenCount":0,"totalTokenCount":0}}`),		[]byte(`data: {"usageMetadata":{"promptTokenCount":17984,"candidatesTokenCount":2668,"thoughtsTokenCount":1028,"totalTokenCount":21680}}`),	} 	accepted := make([]usage.Detail, 0, len(lines))	for _, line := range lines {		detail, ok := ParseGeminiStreamUsage(line)		if ok {			accepted = append(accepted, detail)		}	} 	if len(accepted) != 1 {		t.Fatalf("accepted usage count = %d, want 1", len(accepted))	}	detail := accepted[0]	if detail.InputTokens != 17984 || detail.OutputTokens != 2668 || detail.ReasoningTokens != 1028 || detail.TotalTokens != 21680 {		t.Fatalf("accepted usage detail = %+v", detail)	}} func TestParseGeminiUsageRejectsInvalidToolUseSums(t *testing.T) {	tests := map[string]string{		"negative": `{"usageMetadata":{"promptTokenCount":10,"toolUsePromptTokenCount":-1,"totalTokenCount":10}}`,		"overflow": `{"usageMetadata":{"promptTokenCount":9223372036854775807,"toolUsePromptTokenCount":1,"totalTokenCount":9223372036854775807}}`,	}	for name, payload := range tests {		t.Run(name, func(t *testing.T) {			detail := ParseGeminiUsage([]byte(payload))			if detail.InputTokens < 0 || !detail.TokenBreakdown.Valid() ||				detail.TokenBreakdown.Quality != usage.TokenAccountingQualityInconsistent {				t.Fatalf("detail = %+v", detail)			}		})	}} func TestParseInteractionsUsage(t *testing.T) {	detail := ParseInteractionsUsage([]byte(`{"usage":{"input_tokens":3,"output_tokens":4,"reasoning_tokens":5,"cached_tokens":2}}`))	if detail.InputTokens != 3 {		t.Fatalf("input tokens = %d, want 3", detail.InputTokens)	}	if detail.OutputTokens != 4 {		t.Fatalf("output tokens = %d, want 4", detail.OutputTokens)	}	if detail.ReasoningTokens != 5 {		t.Fatalf("reasoning tokens = %d, want 5", detail.ReasoningTokens)	}	if detail.TotalTokens != 12 {		t.Fatalf("total tokens = %d, want 12", detail.TotalTokens)	}	if detail.CachedTokens != 2 {		t.Fatalf("cached tokens = %d, want 2", detail.CachedTokens)	}	if detail.CacheReadTokens != 2 {		t.Fatalf("cache read tokens = %d, want 2", detail.CacheReadTokens)	}	if detail.TokenBreakdown.Input.UncachedTokens != 1 || detail.TokenBreakdown.Output.TotalTokens != 9 {		t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)	}} func TestNormalizeUsageDetailTotalDoesNotDoubleCountReasoning(t *testing.T) {	detail := normalizeUsageDetailTotal(usage.Detail{		InputTokens:     100,

Lines 1–500 of 1181.